diff options
author | Jelmer Vernooij <jelmer@samba.org> | 2012-12-28 15:37:14 +0100 |
---|---|---|
committer | Andrew Bartlett <abartlet@samba.org> | 2013-03-02 03:57:34 +0100 |
commit | 87afc3aee1ea593069322a49355dd8780d99e123 (patch) | |
tree | 8e1ea6678d93b53f21b34c4940b7d5a64e0f5020 /python/samba | |
parent | 80fce353e740c793619005ac102ab07fb5e7d280 (diff) | |
download | samba-87afc3aee1ea593069322a49355dd8780d99e123.tar.gz |
Move python modules from source4/scripting/python/ to python/.
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
Autobuild-User(master): Andrew Bartlett <abartlet@samba.org>
Autobuild-Date(master): Sat Mar 2 03:57:34 CET 2013 on sn-devel-104
Diffstat (limited to 'python/samba')
103 files changed, 30623 insertions, 0 deletions
diff --git a/python/samba/__init__.py b/python/samba/__init__.py new file mode 100644 index 00000000000..cd2a309fc0a --- /dev/null +++ b/python/samba/__init__.py @@ -0,0 +1,363 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2008 +# +# Based on the original in EJS: +# Copyright (C) Andrew Tridgell <tridge@samba.org> 2005 +# +# 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/>. +# + +"""Samba 4.""" + +__docformat__ = "restructuredText" + +import os +import sys +import samba.param + + +def source_tree_topdir(): + """Return the top level source directory.""" + paths = ["../../..", "../../../.."] + for p in paths: + topdir = os.path.normpath(os.path.join(os.path.dirname(__file__), p)) + if os.path.exists(os.path.join(topdir, 'source4')): + return topdir + raise RuntimeError("unable to find top level source directory") + + +def in_source_tree(): + """Return True if we are running from within the samba source tree""" + try: + topdir = source_tree_topdir() + except RuntimeError: + return False + return True + + +import ldb +from samba._ldb import Ldb as _Ldb + + +class Ldb(_Ldb): + """Simple Samba-specific LDB subclass that takes care + of setting up the modules dir, credentials pointers, etc. + + Please note that this is intended to be for all Samba LDB files, + not necessarily the Sam database. For Sam-specific helper + functions see samdb.py. + """ + + def __init__(self, url=None, lp=None, modules_dir=None, session_info=None, + credentials=None, flags=0, options=None): + """Opens a Samba Ldb file. + + :param url: Optional LDB URL to open + :param lp: Optional loadparm object + :param modules_dir: Optional modules directory + :param session_info: Optional session information + :param credentials: Optional credentials, defaults to anonymous. + :param flags: Optional LDB flags + :param options: Additional options (optional) + + This is different from a regular Ldb file in that the Samba-specific + modules-dir is used by default and that credentials and session_info + can be passed through (required by some modules). + """ + + if modules_dir is not None: + self.set_modules_dir(modules_dir) + else: + self.set_modules_dir(os.path.join(samba.param.modules_dir(), "ldb")) + + if session_info is not None: + self.set_session_info(session_info) + + if credentials is not None: + self.set_credentials(credentials) + + if lp is not None: + self.set_loadparm(lp) + + # This must be done before we load the schema, as these handlers for + # objectSid and objectGUID etc must take precedence over the 'binary + # attribute' declaration in the schema + self.register_samba_handlers() + + # TODO set debug + def msg(l, text): + print text + #self.set_debug(msg) + + self.set_utf8_casefold() + + # Allow admins to force non-sync ldb for all databases + if lp is not None: + nosync_p = lp.get("nosync", "ldb") + if nosync_p is not None and nosync_p: + flags |= ldb.FLG_NOSYNC + + self.set_create_perms(0600) + + if url is not None: + self.connect(url, flags, options) + + def searchone(self, attribute, basedn=None, expression=None, + scope=ldb.SCOPE_BASE): + """Search for one attribute as a string. + + :param basedn: BaseDN for the search. + :param attribute: Name of the attribute + :param expression: Optional search expression. + :param scope: Search scope (defaults to base). + :return: Value of attribute as a string or None if it wasn't found. + """ + res = self.search(basedn, scope, expression, [attribute]) + if len(res) != 1 or res[0][attribute] is None: + return None + values = set(res[0][attribute]) + assert len(values) == 1 + return self.schema_format_value(attribute, values.pop()) + + def erase_users_computers(self, dn): + """Erases user and computer objects from our AD. + + This is needed since the 'samldb' module denies the deletion of primary + groups. Therefore all groups shouldn't be primary somewhere anymore. + """ + + try: + res = self.search(base=dn, scope=ldb.SCOPE_SUBTREE, attrs=[], + expression="(|(objectclass=user)(objectclass=computer))") + except ldb.LdbError, (errno, _): + if errno == ldb.ERR_NO_SUCH_OBJECT: + # Ignore no such object errors + return + else: + raise + + try: + for msg in res: + self.delete(msg.dn, ["relax:0"]) + except ldb.LdbError, (errno, _): + if errno != ldb.ERR_NO_SUCH_OBJECT: + # Ignore no such object errors + raise + + def erase_except_schema_controlled(self): + """Erase this ldb. + + :note: Removes all records, except those that are controlled by + Samba4's schema. + """ + + basedn = "" + + # Try to delete user/computer accounts to allow deletion of groups + self.erase_users_computers(basedn) + + # Delete the 'visible' records, and the invisble 'deleted' records (if + # this DB supports it) + for msg in self.search(basedn, ldb.SCOPE_SUBTREE, + "(&(|(objectclass=*)(distinguishedName=*))(!(distinguishedName=@BASEINFO)))", + [], controls=["show_deleted:0", "show_recycled:0"]): + try: + self.delete(msg.dn, ["relax:0"]) + except ldb.LdbError, (errno, _): + if errno != ldb.ERR_NO_SUCH_OBJECT: + # Ignore no such object errors + raise + + res = self.search(basedn, ldb.SCOPE_SUBTREE, + "(&(|(objectclass=*)(distinguishedName=*))(!(distinguishedName=@BASEINFO)))", + [], controls=["show_deleted:0", "show_recycled:0"]) + assert len(res) == 0 + + # delete the specials + for attr in ["@SUBCLASSES", "@MODULES", + "@OPTIONS", "@PARTITION", "@KLUDGEACL"]: + try: + self.delete(attr, ["relax:0"]) + except ldb.LdbError, (errno, _): + if errno != ldb.ERR_NO_SUCH_OBJECT: + # Ignore missing dn errors + raise + + def erase(self): + """Erase this ldb, removing all records.""" + self.erase_except_schema_controlled() + + # delete the specials + for attr in ["@INDEXLIST", "@ATTRIBUTES"]: + try: + self.delete(attr, ["relax:0"]) + except ldb.LdbError, (errno, _): + if errno != ldb.ERR_NO_SUCH_OBJECT: + # Ignore missing dn errors + raise + + def load_ldif_file_add(self, ldif_path): + """Load a LDIF file. + + :param ldif_path: Path to LDIF file. + """ + self.add_ldif(open(ldif_path, 'r').read()) + + def add_ldif(self, ldif, controls=None): + """Add data based on a LDIF string. + + :param ldif: LDIF text. + """ + for changetype, msg in self.parse_ldif(ldif): + assert changetype == ldb.CHANGETYPE_NONE + self.add(msg, controls) + + def modify_ldif(self, ldif, controls=None): + """Modify database based on a LDIF string. + + :param ldif: LDIF text. + """ + for changetype, msg in self.parse_ldif(ldif): + if changetype == ldb.CHANGETYPE_ADD: + self.add(msg, controls) + else: + self.modify(msg, controls) + + +def substitute_var(text, values): + """Substitute strings of the form ${NAME} in str, replacing + with substitutions from values. + + :param text: Text in which to subsitute. + :param values: Dictionary with keys and values. + """ + + for (name, value) in values.items(): + assert isinstance(name, str), "%r is not a string" % name + assert isinstance(value, str), "Value %r for %s is not a string" % (value, name) + text = text.replace("${%s}" % name, value) + + return text + + +def check_all_substituted(text): + """Check that all substitution variables in a string have been replaced. + + If not, raise an exception. + + :param text: The text to search for substitution variables + """ + if not "${" in text: + return + + var_start = text.find("${") + var_end = text.find("}", var_start) + + raise Exception("Not all variables substituted: %s" % + text[var_start:var_end+1]) + + +def read_and_sub_file(file_name, subst_vars): + """Read a file and sub in variables found in it + + :param file_name: File to be read (typically from setup directory) + param subst_vars: Optional variables to subsitute in the file. + """ + data = open(file_name, 'r').read() + if subst_vars is not None: + data = substitute_var(data, subst_vars) + check_all_substituted(data) + return data + + +def setup_file(template, fname, subst_vars=None): + """Setup a file in the private dir. + + :param template: Path of the template file. + :param fname: Path of the file to create. + :param subst_vars: Substitution variables. + """ + if os.path.exists(fname): + os.unlink(fname) + + data = read_and_sub_file(template, subst_vars) + f = open(fname, 'w') + try: + f.write(data) + finally: + f.close() + +MAX_NETBIOS_NAME_LEN = 15 +def is_valid_netbios_char(c): + return (c.isalnum() or c in " !#$%&'()-.@^_{}~") + + +def valid_netbios_name(name): + """Check whether a name is valid as a NetBIOS name. """ + # See crh's book (1.4.1.1) + if len(name) > MAX_NETBIOS_NAME_LEN: + return False + for x in name: + if not is_valid_netbios_char(x): + return False + return True + + +def import_bundled_package(modulename, location): + """Import the bundled version of a package. + + :note: This should only be called if the system version of the package + is not adequate. + + :param modulename: Module name to import + :param location: Location to add to sys.path (can be relative to + ${srcdir}/lib) + """ + if in_source_tree(): + sys.path.insert(0, os.path.join(source_tree_topdir(), "lib", location)) + sys.modules[modulename] = __import__(modulename) + else: + sys.modules[modulename] = __import__( + "samba.external.%s" % modulename, fromlist=["samba.external"]) + + +def ensure_external_module(modulename, location): + """Add a location to sys.path if an external dependency can't be found. + + :param modulename: Module name to import + :param location: Location to add to sys.path (can be relative to + ${srcdir}/lib) + """ + try: + __import__(modulename) + except ImportError: + import_bundled_package(modulename, location) + + +def dn_from_dns_name(dnsdomain): + """return a DN from a DNS name domain/forest root""" + return "DC=" + ",DC=".join(dnsdomain.split(".")) + +import _glue +version = _glue.version +interface_ips = _glue.interface_ips +set_debug_level = _glue.set_debug_level +get_debug_level = _glue.get_debug_level +unix2nttime = _glue.unix2nttime +nttime2string = _glue.nttime2string +nttime2unix = _glue.nttime2unix +unix2nttime = _glue.unix2nttime +generate_random_password = _glue.generate_random_password +strcasecmp_m = _glue.strcasecmp_m +strstr_m = _glue.strstr_m diff --git a/python/samba/common.py b/python/samba/common.py new file mode 100644 index 00000000000..e47f276f819 --- /dev/null +++ b/python/samba/common.py @@ -0,0 +1,99 @@ +# Samba common functions +# +# Copyright (C) Matthieu Patou <mat@matws.net> +# +# 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 +import dsdb + + +def confirm(msg, forced=False, allow_all=False): + """confirm an action with the user + + :param msg: A string to print to the user + :param forced: Are the answer forced + """ + if forced: + print("%s [YES]" % msg) + return True + + mapping = { + 'Y': True, + 'YES': True, + '': False, + 'N': False, + 'NO': False, + } + + prompt = '[y/N]' + + if allow_all: + mapping['ALL'] = 'ALL' + mapping['NONE'] = 'NONE' + prompt = '[y/N/all/none]' + + while True: + v = raw_input(msg + ' %s ' % prompt) + v = v.upper() + if v in mapping: + return mapping[v] + print("Unknown response '%s'" % v) + + +def normalise_int32(ivalue): + '''normalise a ldap integer to signed 32 bit''' + if int(ivalue) & 0x80000000 and int(ivalue) > 0: + return str(int(ivalue) - 0x100000000) + return str(ivalue) + + +class dsdb_Dn(object): + '''a class for binary DN''' + + def __init__(self, samdb, dnstring, syntax_oid=None): + '''create a dsdb_Dn''' + if syntax_oid is None: + # auto-detect based on string + if dnstring.startswith("B:"): + syntax_oid = dsdb.DSDB_SYNTAX_BINARY_DN + elif dnstring.startswith("S:"): + syntax_oid = dsdb.DSDB_SYNTAX_STRING_DN + else: + syntax_oid = dsdb.DSDB_SYNTAX_OR_NAME + if syntax_oid in [dsdb.DSDB_SYNTAX_BINARY_DN, dsdb.DSDB_SYNTAX_STRING_DN]: + # it is a binary DN + colons = dnstring.split(':') + if len(colons) < 4: + raise RuntimeError("Invalid DN %s" % dnstring) + prefix_len = 4 + len(colons[1]) + int(colons[1]) + self.prefix = dnstring[0:prefix_len] + self.binary = self.prefix[4:-1] + self.dnstring = dnstring[prefix_len:] + else: + self.dnstring = dnstring + self.prefix = '' + self.binary = '' + self.dn = ldb.Dn(samdb, self.dnstring) + + def __str__(self): + return self.prefix + str(self.dn.extended_str(mode=1)) + + def get_binary_integer(self): + '''return binary part of a dsdb_Dn as an integer, or None''' + if self.prefix == '': + return None + return int(self.binary, 16) diff --git a/python/samba/dbchecker.py b/python/samba/dbchecker.py new file mode 100644 index 00000000000..06fd82752f7 --- /dev/null +++ b/python/samba/dbchecker.py @@ -0,0 +1,947 @@ +# Samba4 AD database checker +# +# Copyright (C) Andrew Tridgell 2011 +# Copyright (C) Matthieu Patou <mat@matws.net> 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 +from samba import dsdb +from samba import common +from samba.dcerpc import misc +from samba.ndr import ndr_unpack, ndr_pack +from samba.dcerpc import drsblobs +from samba.common import dsdb_Dn +from samba.dcerpc import security + + +class dbcheck(object): + """check a SAM database for errors""" + + def __init__(self, samdb, samdb_schema=None, verbose=False, fix=False, + yes=False, quiet=False, in_transaction=False): + self.samdb = samdb + self.dict_oid_name = None + self.samdb_schema = (samdb_schema or samdb) + self.verbose = verbose + self.fix = fix + self.yes = yes + self.quiet = quiet + self.remove_all_unknown_attributes = False + self.remove_all_empty_attributes = False + self.fix_all_normalisation = False + self.fix_all_DN_GUIDs = False + self.remove_all_deleted_DN_links = False + self.fix_all_target_mismatch = False + self.fix_all_metadata = False + self.fix_time_metadata = False + self.fix_all_missing_backlinks = False + self.fix_all_orphaned_backlinks = False + self.fix_rmd_flags = False + self.fix_ntsecuritydescriptor = False + self.seize_fsmo_role = False + self.move_to_lost_and_found = False + self.fix_instancetype = False + self.in_transaction = in_transaction + self.infrastructure_dn = ldb.Dn(samdb, "CN=Infrastructure," + samdb.domain_dn()) + self.naming_dn = ldb.Dn(samdb, "CN=Partitions,%s" % samdb.get_config_basedn()) + self.schema_dn = samdb.get_schema_basedn() + self.rid_dn = ldb.Dn(samdb, "CN=RID Manager$,CN=System," + samdb.domain_dn()) + self.ntds_dsa = samdb.get_dsServiceName() + self.class_schemaIDGUID = {} + + res = self.samdb.search(base=self.ntds_dsa, scope=ldb.SCOPE_BASE, attrs=['msDS-hasMasterNCs', 'hasMasterNCs']) + if "msDS-hasMasterNCs" in res[0]: + self.write_ncs = res[0]["msDS-hasMasterNCs"] + else: + # If the Forest Level is less than 2003 then there is no + # msDS-hasMasterNCs, so we fall back to hasMasterNCs + # no need to merge as all the NCs that are in hasMasterNCs must + # also be in msDS-hasMasterNCs (but not the opposite) + if "hasMasterNCs" in res[0]: + self.write_ncs = res[0]["hasMasterNCs"] + else: + self.write_ncs = None + + + def check_database(self, DN=None, scope=ldb.SCOPE_SUBTREE, controls=[], attrs=['*']): + '''perform a database check, returning the number of errors found''' + + res = self.samdb.search(base=DN, scope=scope, attrs=['dn'], controls=controls) + self.report('Checking %u objects' % len(res)) + error_count = 0 + + for object in res: + error_count += self.check_object(object.dn, attrs=attrs) + + if DN is None: + error_count += self.check_rootdse() + + if error_count != 0 and not self.fix: + self.report("Please use --fix to fix these errors") + + self.report('Checked %u objects (%u errors)' % (len(res), error_count)) + return error_count + + def report(self, msg): + '''print a message unless quiet is set''' + if not self.quiet: + print(msg) + + def confirm(self, msg, allow_all=False, forced=False): + '''confirm a change''' + if not self.fix: + return False + if self.quiet: + return self.yes + if self.yes: + forced = True + return common.confirm(msg, forced=forced, allow_all=allow_all) + + ################################################################ + # a local confirm function with support for 'all' + def confirm_all(self, msg, all_attr): + '''confirm a change with support for "all" ''' + if not self.fix: + return False + if self.quiet: + return self.yes + if getattr(self, all_attr) == 'NONE': + return False + if getattr(self, all_attr) == 'ALL': + forced = True + else: + forced = self.yes + c = common.confirm(msg, forced=forced, allow_all=True) + if c == 'ALL': + setattr(self, all_attr, 'ALL') + return True + if c == 'NONE': + setattr(self, all_attr, 'NONE') + return False + return c + + def do_modify(self, m, controls, msg, validate=True): + '''perform a modify with optional verbose output''' + if self.verbose: + self.report(self.samdb.write_ldif(m, ldb.CHANGETYPE_MODIFY)) + try: + controls = controls + ["local_oid:%s:0" % dsdb.DSDB_CONTROL_DBCHECK] + self.samdb.modify(m, controls=controls, validate=validate) + except Exception, err: + self.report("%s : %s" % (msg, err)) + return False + return True + + def do_rename(self, from_dn, to_rdn, to_base, controls, msg): + '''perform a modify with optional verbose output''' + if self.verbose: + self.report("""dn: %s +changeType: modrdn +newrdn: %s +deleteOldRdn: 1 +newSuperior: %s""" % (str(from_dn), str(to_rdn), str(to_base))) + try: + to_dn = to_rdn + to_base + controls = controls + ["local_oid:%s:0" % dsdb.DSDB_CONTROL_DBCHECK] + self.samdb.rename(from_dn, to_dn, controls=controls) + except Exception, err: + self.report("%s : %s" % (msg, err)) + return False + return True + + def err_empty_attribute(self, dn, attrname): + '''fix empty attributes''' + self.report("ERROR: Empty attribute %s in %s" % (attrname, dn)) + if not self.confirm_all('Remove empty attribute %s from %s?' % (attrname, dn), 'remove_all_empty_attributes'): + self.report("Not fixing empty attribute %s" % attrname) + return + + m = ldb.Message() + m.dn = dn + m[attrname] = ldb.MessageElement('', ldb.FLAG_MOD_DELETE, attrname) + if self.do_modify(m, ["relax:0", "show_recycled:1"], + "Failed to remove empty attribute %s" % attrname, validate=False): + self.report("Removed empty attribute %s" % attrname) + + def err_normalise_mismatch(self, dn, attrname, values): + '''fix attribute normalisation errors''' + self.report("ERROR: Normalisation error for attribute %s in %s" % (attrname, dn)) + mod_list = [] + for val in values: + normalised = self.samdb.dsdb_normalise_attributes( + self.samdb_schema, attrname, [val]) + if len(normalised) != 1: + self.report("Unable to normalise value '%s'" % val) + mod_list.append((val, '')) + elif (normalised[0] != val): + self.report("value '%s' should be '%s'" % (val, normalised[0])) + mod_list.append((val, normalised[0])) + if not self.confirm_all('Fix normalisation for %s from %s?' % (attrname, dn), 'fix_all_normalisation'): + self.report("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) + + if self.do_modify(m, ["relax:0", "show_recycled:1"], + "Failed to normalise attribute %s" % attrname, + validate=False): + self.report("Normalised attribute %s" % attrname) + + def err_normalise_mismatch_replace(self, dn, attrname, values): + '''fix attribute normalisation errors''' + normalised = self.samdb.dsdb_normalise_attributes(self.samdb_schema, attrname, values) + self.report("ERROR: Normalisation error for attribute '%s' in '%s'" % (attrname, dn)) + self.report("Values/Order of values do/does not match: %s/%s!" % (values, list(normalised))) + if list(normalised) == values: + return + if not self.confirm_all("Fix normalisation for '%s' from '%s'?" % (attrname, dn), 'fix_all_normalisation'): + self.report("Not fixing attribute '%s'" % attrname) + return + + m = ldb.Message() + m.dn = dn + m[attrname] = ldb.MessageElement(normalised, ldb.FLAG_MOD_REPLACE, attrname) + + if self.do_modify(m, ["relax:0", "show_recycled:1"], + "Failed to normalise attribute %s" % attrname, + validate=False): + self.report("Normalised attribute %s" % attrname) + + def is_deleted_objects_dn(self, dsdb_dn): + '''see if a dsdb_Dn is the special Deleted Objects DN''' + return dsdb_dn.prefix == "B:32:18E2EA80684F11D2B9AA00C04F79F805:" + + def err_deleted_dn(self, dn, attrname, val, dsdb_dn, correct_dn): + """handle a DN pointing to a deleted object""" + 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) + if not self.confirm_all('Remove DN link?', 'remove_all_deleted_DN_links'): + self.report("Not removing") + return + m = ldb.Message() + m.dn = dn + m['old_value'] = ldb.MessageElement(val, ldb.FLAG_MOD_DELETE, attrname) + if self.do_modify(m, ["show_recycled:1", "local_oid:%s:0" % dsdb.DSDB_CONTROL_DBCHECK], + "Failed to remove deleted DN attribute %s" % attrname): + self.report("Removed deleted DN on attribute %s" % attrname) + + def err_missing_dn_GUID(self, dn, attrname, val, dsdb_dn): + """handle a missing target DN (both GUID and DN string form are missing)""" + # check if its a backlink + linkID = self.samdb_schema.get_linkId_from_lDAPDisplayName(attrname) + if (linkID & 1 == 0) and str(dsdb_dn).find('DEL\\0A') == -1: + self.report("Not removing dangling forward link") + return + self.err_deleted_dn(dn, attrname, val, dsdb_dn, dsdb_dn) + + def err_incorrect_dn_GUID(self, dn, attrname, val, dsdb_dn, errstr): + """handle a missing GUID extended DN component""" + self.report("ERROR: %s component for %s in object %s - %s" % (errstr, attrname, dn, val)) + controls=["extended_dn:1:1", "show_recycled:1"] + try: + res = self.samdb.search(base=str(dsdb_dn.dn), scope=ldb.SCOPE_BASE, + attrs=[], controls=controls) + except ldb.LdbError, (enum, estr): + self.report("unable to find object for DN %s - (%s)" % (dsdb_dn.dn, estr)) + self.err_missing_dn_GUID(dn, attrname, val, dsdb_dn) + return + if len(res) == 0: + self.report("unable to find object for DN %s" % dsdb_dn.dn) + self.err_missing_dn_GUID(dn, attrname, val, dsdb_dn) + return + dsdb_dn.dn = res[0].dn + + if not self.confirm_all('Change DN to %s?' % str(dsdb_dn), 'fix_all_DN_GUIDs'): + self.report("Not fixing %s" % errstr) + 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.do_modify(m, ["show_recycled:1"], + "Failed to fix %s on attribute %s" % (errstr, attrname)): + self.report("Fixed %s on attribute %s" % (errstr, attrname)) + + def err_dn_target_mismatch(self, dn, attrname, val, dsdb_dn, correct_dn, errstr): + """handle a DN string being incorrect""" + self.report("ERROR: incorrect DN string component for %s in object %s - %s" % (attrname, dn, val)) + dsdb_dn.dn = correct_dn + + if not self.confirm_all('Change DN to %s?' % str(dsdb_dn), 'fix_all_target_mismatch'): + self.report("Not fixing %s" % errstr) + 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.do_modify(m, ["show_recycled:1"], + "Failed to fix incorrect DN string on attribute %s" % attrname): + self.report("Fixed incorrect DN string on attribute %s" % (attrname)) + + def err_unknown_attribute(self, obj, attrname): + '''handle an unknown attribute error''' + self.report("ERROR: unknown attribute '%s' in %s" % (attrname, obj.dn)) + if not self.confirm_all('Remove unknown attribute %s' % attrname, 'remove_all_unknown_attributes'): + self.report("Not removing %s" % attrname) + return + m = ldb.Message() + m.dn = obj.dn + m['old_value'] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE, attrname) + if self.do_modify(m, ["relax:0", "show_recycled:1"], + "Failed to remove unknown attribute %s" % attrname): + self.report("Removed unknown attribute %s" % (attrname)) + + def err_missing_backlink(self, obj, attrname, val, backlink_name, target_dn): + '''handle a missing backlink value''' + self.report("ERROR: missing backlink attribute '%s' in %s for link %s in %s" % (backlink_name, target_dn, attrname, obj.dn)) + if not self.confirm_all('Fix missing backlink %s' % backlink_name, 'fix_all_missing_backlinks'): + self.report("Not fixing missing backlink %s" % backlink_name) + return + m = ldb.Message() + m.dn = obj.dn + m['old_value'] = ldb.MessageElement(val, ldb.FLAG_MOD_DELETE, attrname) + m['new_value'] = ldb.MessageElement(val, ldb.FLAG_MOD_ADD, attrname) + if self.do_modify(m, ["show_recycled:1"], + "Failed to fix missing backlink %s" % backlink_name): + self.report("Fixed missing backlink %s" % (backlink_name)) + + def err_incorrect_rmd_flags(self, obj, attrname, revealed_dn): + '''handle a incorrect RMD_FLAGS value''' + rmd_flags = int(revealed_dn.dn.get_extended_component("RMD_FLAGS")) + self.report("ERROR: incorrect RMD_FLAGS value %u for attribute '%s' in %s for link %s" % (rmd_flags, attrname, obj.dn, revealed_dn.dn.extended_str())) + if not self.confirm_all('Fix incorrect RMD_FLAGS %u' % rmd_flags, 'fix_rmd_flags'): + self.report("Not fixing incorrect RMD_FLAGS %u" % rmd_flags) + return + m = ldb.Message() + m.dn = obj.dn + m['old_value'] = ldb.MessageElement(str(revealed_dn), ldb.FLAG_MOD_DELETE, attrname) + if self.do_modify(m, ["show_recycled:1", "reveal_internals:0", "show_deleted:0"], + "Failed to fix incorrect RMD_FLAGS %u" % rmd_flags): + self.report("Fixed incorrect RMD_FLAGS %u" % (rmd_flags)) + + def err_orphaned_backlink(self, obj, attrname, val, link_name, target_dn): + '''handle a orphaned backlink value''' + self.report("ERROR: orphaned backlink attribute '%s' in %s for link %s in %s" % (attrname, obj.dn, link_name, target_dn)) + if not self.confirm_all('Remove orphaned backlink %s' % link_name, 'fix_all_orphaned_backlinks'): + self.report("Not removing orphaned backlink %s" % link_name) + return + m = ldb.Message() + m.dn = obj.dn + m['value'] = ldb.MessageElement(val, ldb.FLAG_MOD_DELETE, attrname) + if self.do_modify(m, ["show_recycled:1", "relax:0"], + "Failed to fix orphaned backlink %s" % link_name): + self.report("Fixed orphaned backlink %s" % (link_name)) + + def err_no_fsmoRoleOwner(self, obj): + '''handle a missing fSMORoleOwner''' + self.report("ERROR: fSMORoleOwner not found for role %s" % (obj.dn)) + res = self.samdb.search("", + scope=ldb.SCOPE_BASE, attrs=["dsServiceName"]) + assert len(res) == 1 + serviceName = res[0]["dsServiceName"][0] + if not self.confirm_all('Sieze role %s onto current DC by adding fSMORoleOwner=%s' % (obj.dn, serviceName), 'seize_fsmo_role'): + self.report("Not Siezing role %s onto current DC by adding fSMORoleOwner=%s" % (obj.dn, serviceName)) + return + m = ldb.Message() + m.dn = obj.dn + m['value'] = ldb.MessageElement(serviceName, ldb.FLAG_MOD_ADD, 'fSMORoleOwner') + if self.do_modify(m, [], + "Failed to sieze role %s onto current DC by adding fSMORoleOwner=%s" % (obj.dn, serviceName)): + self.report("Siezed role %s onto current DC by adding fSMORoleOwner=%s" % (obj.dn, serviceName)) + + def err_missing_parent(self, obj): + '''handle a missing parent''' + self.report("ERROR: parent object not found for %s" % (obj.dn)) + if not self.confirm_all('Move object %s into LostAndFound?' % (obj.dn), 'move_to_lost_and_found'): + self.report('Not moving object %s into LostAndFound' % (obj.dn)) + return + + keep_transaction = True + self.samdb.transaction_start() + try: + nc_root = self.samdb.get_nc_root(obj.dn); + lost_and_found = self.samdb.get_wellknown_dn(nc_root, dsdb.DS_GUID_LOSTANDFOUND_CONTAINER) + new_dn = ldb.Dn(self.samdb, str(obj.dn)) + new_dn.remove_base_components(len(new_dn) - 1) + if self.do_rename(obj.dn, new_dn, lost_and_found, ["show_deleted:0", "relax:0"], + "Failed to rename object %s into lostAndFound at %s" % (obj.dn, new_dn + lost_and_found)): + self.report("Renamed object %s into lostAndFound at %s" % (obj.dn, new_dn + lost_and_found)) + + m = ldb.Message() + m.dn = obj.dn + m['lastKnownParent'] = ldb.MessageElement(str(obj.dn.parent()), ldb.FLAG_MOD_REPLACE, 'lastKnownParent') + + if self.do_modify(m, [], + "Failed to set lastKnownParent on lostAndFound object at %s" % (new_dn + lost_and_found)): + self.report("Set lastKnownParent on lostAndFound object at %s" % (new_dn + lost_and_found)) + keep_transaction = True + except: + self.samdb.transaction_cancel() + raise + + if keep_transaction: + self.samdb.transaction_commit() + else: + self.samdb.transaction_cancel() + + + def err_wrong_instancetype(self, obj, calculated_instancetype): + '''handle a wrong instanceType''' + self.report("ERROR: wrong instanceType %s on %s, should be %d" % (obj["instanceType"], obj.dn, calculated_instancetype)) + if not self.confirm_all('Change instanceType from %s to %d on %s?' % (obj["instanceType"], calculated_instancetype, obj.dn), 'fix_instancetype'): + self.report('Not changing instanceType from %s to %d on %s' % (obj["instanceType"], calculated_instancetype, obj.dn)) + return + + m = ldb.Message() + m.dn = obj.dn + m['value'] = ldb.MessageElement(str(calculated_instancetype), ldb.FLAG_MOD_REPLACE, 'instanceType') + if self.do_modify(m, ["local_oid:%s:0" % dsdb.DSDB_CONTROL_DBCHECK_MODIFY_RO_REPLICA], + "Failed to correct missing instanceType on %s by setting instanceType=%d" % (obj.dn, calculated_instancetype)): + self.report("Corrected instancetype on %s by setting instanceType=%d" % (obj.dn, calculated_instancetype)) + + def find_revealed_link(self, dn, attrname, guid): + '''return a revealed link in an object''' + res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE, attrs=[attrname], + controls=["show_deleted:0", "extended_dn:0", "reveal_internals:0"]) + syntax_oid = self.samdb_schema.get_syntax_oid_from_lDAPDisplayName(attrname) + for val in res[0][attrname]: + dsdb_dn = dsdb_Dn(self.samdb, val, syntax_oid) + guid2 = dsdb_dn.dn.get_extended_component("GUID") + if guid == guid2: + return dsdb_dn + return None + + 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 + self.err_incorrect_dn_GUID(obj.dn, attrname, val, dsdb_dn, + "missing GUID") + continue + + guidstr = str(misc.GUID(guid)) + + attrs = ['isDeleted'] + linkID = self.samdb_schema.get_linkId_from_lDAPDisplayName(attrname) + reverse_link_name = self.samdb_schema.get_backlink_from_lDAPDisplayName(attrname) + if reverse_link_name is not None: + attrs.append(reverse_link_name) + + # check its the right GUID + try: + res = self.samdb.search(base="<GUID=%s>" % guidstr, scope=ldb.SCOPE_BASE, + attrs=attrs, controls=["extended_dn:1:1", "show_recycled:1"]) + except ldb.LdbError, (enum, estr): + error_count += 1 + self.err_incorrect_dn_GUID(obj.dn, attrname, val, dsdb_dn, "incorrect GUID") + continue + + # now we have two cases - the source object might or might not be deleted + is_deleted = 'isDeleted' in obj and obj['isDeleted'][0].upper() == 'TRUE' + target_is_deleted = 'isDeleted' in res[0] and res[0]['isDeleted'][0].upper() == 'TRUE' + + # the target DN is not allowed to be deleted, unless the target DN is the + # special Deleted Objects container + if target_is_deleted and not is_deleted and not self.is_deleted_objects_dn(dsdb_dn): + 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, "incorrect string version of DN") + continue + + if is_deleted and not target_is_deleted and reverse_link_name is not None: + revealed_dn = self.find_revealed_link(obj.dn, attrname, guid) + rmd_flags = revealed_dn.dn.get_extended_component("RMD_FLAGS") + if rmd_flags is not None and (int(rmd_flags) & 1) == 0: + # the RMD_FLAGS for this link should be 1, as the target is deleted + self.err_incorrect_rmd_flags(obj, attrname, revealed_dn) + continue + + # check the reverse_link is correct if there should be one + if reverse_link_name is not None: + match_count = 0 + if reverse_link_name in res[0]: + for v in res[0][reverse_link_name]: + if v == obj.dn.extended_str(): + match_count += 1 + if match_count != 1: + error_count += 1 + if linkID & 1: + self.err_orphaned_backlink(obj, attrname, val, reverse_link_name, dsdb_dn.dn) + else: + self.err_missing_backlink(obj, attrname, val, reverse_link_name, dsdb_dn.dn) + continue + + return error_count + + + def get_originating_time(self, val, attid): + '''Read metadata properties and return the originating time for + a given attributeId. + + :return: the originating time or 0 if not found + ''' + + repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob, str(val)) + obj = repl.ctr + + for o in repl.ctr.array: + if o.attid == attid: + return o.originating_change_time + + return 0 + + def process_metadata(self, val): + '''Read metadata properties and list attributes in it''' + + list_att = [] + + repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob, str(val)) + obj = repl.ctr + + for o in repl.ctr.array: + att = self.samdb_schema.get_lDAPDisplayName_by_attid(o.attid) + list_att.append(att.lower()) + + return list_att + + + def fix_metadata(self, dn, attr): + '''re-write replPropertyMetaData elements for a single attribute for a + object. This is used to fix missing replPropertyMetaData elements''' + res = self.samdb.search(base = dn, scope=ldb.SCOPE_BASE, attrs = [attr], + controls = ["search_options:1:2", "show_recycled:1"]) + msg = res[0] + nmsg = ldb.Message() + nmsg.dn = dn + nmsg[attr] = ldb.MessageElement(msg[attr], ldb.FLAG_MOD_REPLACE, attr) + if self.do_modify(nmsg, ["relax:0", "provision:0", "show_recycled:1"], + "Failed to fix metadata for attribute %s" % attr): + self.report("Fixed metadata for attribute %s" % attr) + + def ace_get_effective_inherited_type(self, ace): + if ace.flags & security.SEC_ACE_FLAG_INHERIT_ONLY: + return None + + check = False + if ace.type == security.SEC_ACE_TYPE_ACCESS_ALLOWED_OBJECT: + check = True + elif ace.type == security.SEC_ACE_TYPE_ACCESS_DENIED_OBJECT: + check = True + elif ace.type == security.SEC_ACE_TYPE_SYSTEM_AUDIT_OBJECT: + check = True + elif ace.type == security.SEC_ACE_TYPE_SYSTEM_ALARM_OBJECT: + check = True + + if not check: + return None + + if not ace.object.flags & security.SEC_ACE_INHERITED_OBJECT_TYPE_PRESENT: + return None + + return str(ace.object.inherited_type) + + def lookup_class_schemaIDGUID(self, cls): + if cls in self.class_schemaIDGUID: + return self.class_schemaIDGUID[cls] + + flt = "(&(ldapDisplayName=%s)(objectClass=classSchema))" % cls + res = self.samdb.search(base=self.schema_dn, + expression=flt, + attrs=["schemaIDGUID"]) + t = str(ndr_unpack(misc.GUID, res[0]["schemaIDGUID"][0])) + + self.class_schemaIDGUID[cls] = t + return t + + def process_sd(self, dn, obj): + sd_attr = "nTSecurityDescriptor" + sd_val = obj[sd_attr] + + sd = ndr_unpack(security.descriptor, str(sd_val)) + + is_deleted = 'isDeleted' in obj and obj['isDeleted'][0].upper() == 'TRUE' + if is_deleted: + # we don't fix deleted objects + return (sd, None) + + sd_clean = security.descriptor() + sd_clean.owner_sid = sd.owner_sid + sd_clean.group_sid = sd.group_sid + sd_clean.type = sd.type + sd_clean.revision = sd.revision + + broken = False + last_inherited_type = None + + aces = [] + if sd.sacl is not None: + aces = sd.sacl.aces + for i in range(0, len(aces)): + ace = aces[i] + + if not ace.flags & security.SEC_ACE_FLAG_INHERITED_ACE: + sd_clean.sacl_add(ace) + continue + + t = self.ace_get_effective_inherited_type(ace) + if t is None: + continue + + if last_inherited_type is not None: + if t != last_inherited_type: + # if it inherited from more than + # one type it's very likely to be broken + # + # If not the recalculation will calculate + # the same result. + broken = True + continue + + last_inherited_type = t + + aces = [] + if sd.dacl is not None: + aces = sd.dacl.aces + for i in range(0, len(aces)): + ace = aces[i] + + if not ace.flags & security.SEC_ACE_FLAG_INHERITED_ACE: + sd_clean.dacl_add(ace) + continue + + t = self.ace_get_effective_inherited_type(ace) + if t is None: + continue + + if last_inherited_type is not None: + if t != last_inherited_type: + # if it inherited from more than + # one type it's very likely to be broken + # + # If not the recalculation will calculate + # the same result. + broken = True + continue + + last_inherited_type = t + + if broken: + return (sd_clean, sd) + + if last_inherited_type is None: + # ok + return (sd, None) + + cls = None + try: + cls = obj["objectClass"][-1] + except KeyError, e: + pass + + if cls is None: + res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE, + attrs=["isDeleted", "objectClass"], + controls=["show_recycled:1"]) + o = res[0] + is_deleted = 'isDeleted' in o and o['isDeleted'][0].upper() == 'TRUE' + if is_deleted: + # we don't fix deleted objects + return (sd, None) + cls = o["objectClass"][-1] + + t = self.lookup_class_schemaIDGUID(cls) + + if t != last_inherited_type: + # broken + return (sd_clean, sd) + + # ok + return (sd, None) + + def err_wrong_sd(self, dn, sd, sd_broken): + '''re-write replPropertyMetaData elements for a single attribute for a + object. This is used to fix missing replPropertyMetaData elements''' + sd_attr = "nTSecurityDescriptor" + sd_val = ndr_pack(sd) + sd_flags = security.SECINFO_DACL | security.SECINFO_SACL + + if not self.confirm_all('Fix %s on %s?' % (sd_attr, dn), 'fix_ntsecuritydescriptor'): + self.report('Not fixing %s on %s\n' % (sd_attr, dn)) + return + + nmsg = ldb.Message() + nmsg.dn = dn + nmsg[sd_attr] = ldb.MessageElement(sd_val, ldb.FLAG_MOD_REPLACE, sd_attr) + if self.do_modify(nmsg, ["sd_flags:1:%d" % sd_flags], + "Failed to fix metadata for attribute %s" % sd_attr): + self.report("Fixed attribute '%s' of '%s'\n" % (sd_attr, dn)) + + def is_fsmo_role(self, dn): + if dn == self.samdb.domain_dn: + return True + if dn == self.infrastructure_dn: + return True + if dn == self.naming_dn: + return True + if dn == self.schema_dn: + return True + if dn == self.rid_dn: + return True + + return False + + def calculate_instancetype(self, dn): + instancetype = 0 + nc_root = self.samdb.get_nc_root(dn) + if dn == nc_root: + instancetype |= dsdb.INSTANCE_TYPE_IS_NC_HEAD + try: + self.samdb.search(base=dn.parent(), scope=ldb.SCOPE_BASE, attrs=[], controls=["show_recycled:1"]) + except ldb.LdbError, (enum, estr): + if enum != ldb.ERR_NO_SUCH_OBJECT: + raise + else: + instancetype |= dsdb.INSTANCE_TYPE_NC_ABOVE + + if self.write_ncs is not None and str(nc_root) in self.write_ncs: + instancetype |= dsdb.INSTANCE_TYPE_WRITE + + return instancetype + + def check_object(self, dn, attrs=['*']): + '''check one object''' + if self.verbose: + self.report("Checking object %s" % dn) + if '*' in attrs: + attrs.append("replPropertyMetaData") + + try: + sd_flags = 0 + sd_flags |= security.SECINFO_OWNER + sd_flags |= security.SECINFO_GROUP + sd_flags |= security.SECINFO_DACL + sd_flags |= security.SECINFO_SACL + + res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE, + controls=[ + "extended_dn:1:1", + "show_recycled:1", + "show_deleted:1", + "sd_flags:1:%d" % sd_flags, + ], + attrs=attrs) + except ldb.LdbError, (enum, estr): + if enum == ldb.ERR_NO_SUCH_OBJECT: + if self.in_transaction: + self.report("ERROR: Object %s disappeared during check" % dn) + return 1 + return 0 + raise + if len(res) != 1: + self.report("ERROR: Object %s failed to load during check" % dn) + return 1 + obj = res[0] + error_count = 0 + list_attrs_from_md = [] + list_attrs_seen = [] + got_repl_property_meta_data = False + + for attrname in obj: + if attrname == 'dn': + continue + + if str(attrname).lower() == 'replpropertymetadata': + list_attrs_from_md = self.process_metadata(obj[attrname]) + got_repl_property_meta_data = True + continue + + if str(attrname).lower() == 'ntsecuritydescriptor': + (sd, sd_broken) = self.process_sd(dn, obj) + if sd_broken is not None: + self.err_wrong_sd(dn, sd, sd_broken) + error_count += 1 + continue + + if str(attrname).lower() == 'objectclass': + normalised = self.samdb.dsdb_normalise_attributes(self.samdb_schema, attrname, list(obj[attrname])) + if list(normalised) != list(obj[attrname]): + self.err_normalise_mismatch_replace(dn, attrname, list(obj[attrname])) + error_count += 1 + continue + + # check for empty attributes + for val in obj[attrname]: + if val == '': + self.err_empty_attribute(dn, attrname) + error_count += 1 + continue + + # get the syntax oid for the attribute, so we can can have + # special handling for some specific attribute types + try: + syntax_oid = self.samdb_schema.get_syntax_oid_from_lDAPDisplayName(attrname) + except Exception, msg: + self.err_unknown_attribute(obj, attrname) + error_count += 1 + continue + + flag = self.samdb_schema.get_systemFlags_from_lDAPDisplayName(attrname) + if (not flag & dsdb.DS_FLAG_ATTR_NOT_REPLICATED + and not flag & dsdb.DS_FLAG_ATTR_IS_CONSTRUCTED + and not self.samdb_schema.get_linkId_from_lDAPDisplayName(attrname)): + list_attrs_seen.append(str(attrname).lower()) + + if syntax_oid in [ dsdb.DSDB_SYNTAX_BINARY_DN, dsdb.DSDB_SYNTAX_OR_NAME, + dsdb.DSDB_SYNTAX_STRING_DN, ldb.SYNTAX_DN ]: + # it's some form of DN, do specialised checking on those + error_count += self.check_dn(obj, attrname, syntax_oid) + + # check for incorrectly normalised attributes + for val in obj[attrname]: + normalised = self.samdb.dsdb_normalise_attributes(self.samdb_schema, attrname, [val]) + if len(normalised) != 1 or normalised[0] != val: + self.err_normalise_mismatch(dn, attrname, obj[attrname]) + error_count += 1 + break + + if str(attrname).lower() == "instancetype": + calculated_instancetype = self.calculate_instancetype(dn) + if len(obj["instanceType"]) != 1 or obj["instanceType"][0] != str(calculated_instancetype): + self.err_wrong_instancetype(obj, calculated_instancetype) + + show_dn = True + if got_repl_property_meta_data: + rdn = (str(dn).split(","))[0] + if rdn == "CN=Deleted Objects": + isDeletedAttId = 131120 + # It's 29/12/9999 at 23:59:59 UTC as specified in MS-ADTS 7.1.1.4.2 Deleted Objects Container + + expectedTimeDo = 2650466015990000000 + originating = self.get_originating_time(obj["replPropertyMetaData"], isDeletedAttId) + if originating != expectedTimeDo: + if self.confirm_all("Fix isDeleted originating_change_time on '%s'" % str(dn), 'fix_time_metadata'): + nmsg = ldb.Message() + nmsg.dn = dn + nmsg["isDeleted"] = ldb.MessageElement("TRUE", ldb.FLAG_MOD_REPLACE, "isDeleted") + error_count += 1 + self.samdb.modify(nmsg, controls=["provision:0"]) + + else: + self.report("Not fixing isDeleted originating_change_time on '%s'" % str(dn)) + for att in list_attrs_seen: + if not att in list_attrs_from_md: + if show_dn: + self.report("On object %s" % dn) + show_dn = False + error_count += 1 + self.report("ERROR: Attribute %s not present in replication metadata" % att) + if not self.confirm_all("Fix missing replPropertyMetaData element '%s'" % att, 'fix_all_metadata'): + self.report("Not fixing missing replPropertyMetaData element '%s'" % att) + continue + self.fix_metadata(dn, att) + + if self.is_fsmo_role(dn): + if "fSMORoleOwner" not in obj: + self.err_no_fsmoRoleOwner(obj) + error_count += 1 + + try: + if dn != self.samdb.get_root_basedn(): + res = self.samdb.search(base=dn.parent(), scope=ldb.SCOPE_BASE, + controls=["show_recycled:1", "show_deleted:1"]) + except ldb.LdbError, (enum, estr): + if enum == ldb.ERR_NO_SUCH_OBJECT: + self.err_missing_parent(obj) + error_count += 1 + else: + raise + + return error_count + + ################################################################ + # check special @ROOTDSE attributes + def check_rootdse(self): + '''check the @ROOTDSE special object''' + dn = ldb.Dn(self.samdb, '@ROOTDSE') + if self.verbose: + self.report("Checking object %s" % dn) + res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE) + if len(res) != 1: + self.report("Object %s disappeared during check" % dn) + return 1 + obj = res[0] + error_count = 0 + + # check that the dsServiceName is in GUID form + if not 'dsServiceName' in obj: + self.report('ERROR: dsServiceName missing in @ROOTDSE') + return error_count+1 + + if not obj['dsServiceName'][0].startswith('<GUID='): + self.report('ERROR: dsServiceName not in GUID form in @ROOTDSE') + error_count += 1 + if not self.confirm('Change dsServiceName to GUID form?'): + return error_count + res = self.samdb.search(base=ldb.Dn(self.samdb, obj['dsServiceName'][0]), + scope=ldb.SCOPE_BASE, attrs=['objectGUID']) + guid_str = str(ndr_unpack(misc.GUID, res[0]['objectGUID'][0])) + m = ldb.Message() + m.dn = dn + m['dsServiceName'] = ldb.MessageElement("<GUID=%s>" % guid_str, + ldb.FLAG_MOD_REPLACE, 'dsServiceName') + if self.do_modify(m, [], "Failed to change dsServiceName to GUID form", validate=False): + self.report("Changed dsServiceName to GUID form") + return error_count + + + ############################################### + # re-index the database + def reindex_database(self): + '''re-index the whole database''' + m = ldb.Message() + m.dn = ldb.Dn(self.samdb, "@ATTRIBUTES") + m['add'] = ldb.MessageElement('NONE', ldb.FLAG_MOD_ADD, 'force_reindex') + m['delete'] = ldb.MessageElement('NONE', ldb.FLAG_MOD_DELETE, 'force_reindex') + return self.do_modify(m, [], 're-indexed database', validate=False) + + ############################################### + # reset @MODULES + def reset_modules(self): + '''reset @MODULES to that needed for current sam.ldb (to read a very old database)''' + m = ldb.Message() + m.dn = ldb.Dn(self.samdb, "@MODULES") + m['@LIST'] = ldb.MessageElement('samba_dsdb', ldb.FLAG_MOD_REPLACE, '@LIST') + return self.do_modify(m, [], 'reset @MODULES on database', validate=False) diff --git a/python/samba/drs_utils.py b/python/samba/drs_utils.py new file mode 100644 index 00000000000..6e2cfea9ab2 --- /dev/null +++ b/python/samba/drs_utils.py @@ -0,0 +1,255 @@ +# DRS utility code +# +# Copyright Andrew Tridgell 2010 +# Copyright Andrew Bartlett 2010 +# +# 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/>. +# + +from samba.dcerpc import drsuapi, misc +from samba.net import Net +import samba, ldb + + +class drsException(Exception): + """Base element for drs errors""" + + def __init__(self, value): + self.value = value + + def __str__(self): + return "drsException: " + self.value + + +def drsuapi_connect(server, lp, creds): + """Make a DRSUAPI connection to the server. + + :param server: the name of the server to connect to + :param lp: a samba line parameter object + :param creds: credential used for the connection + :return: A tuple with the drsuapi bind object, the drsuapi handle + and the supported extensions. + :raise drsException: if the connection fails + """ + + binding_options = "seal" + if int(lp.get("log level")) >= 5: + binding_options += ",print" + binding_string = "ncacn_ip_tcp:%s[%s]" % (server, binding_options) + try: + drsuapiBind = drsuapi.drsuapi(binding_string, lp, creds) + (drsuapiHandle, bindSupportedExtensions) = drs_DsBind(drsuapiBind) + except Exception, e: + raise drsException("DRS connection to %s failed: %s" % (server, e)) + + return (drsuapiBind, drsuapiHandle, bindSupportedExtensions) + + +def sendDsReplicaSync(drsuapiBind, drsuapi_handle, source_dsa_guid, + naming_context, req_option): + """Send DS replica sync request. + + :param drsuapiBind: a drsuapi Bind object + :param drsuapi_handle: a drsuapi hanle on the drsuapi connection + :param source_dsa_guid: the guid of the source dsa for the replication + :param naming_context: the DN of the naming context to replicate + :param req_options: replication options for the DsReplicaSync call + :raise drsException: if any error occur while sending and receiving the + reply for the dsReplicaSync + """ + + nc = drsuapi.DsReplicaObjectIdentifier() + nc.dn = naming_context + + req1 = drsuapi.DsReplicaSyncRequest1() + req1.naming_context = nc; + req1.options = req_option + req1.source_dsa_guid = misc.GUID(source_dsa_guid) + + try: + drsuapiBind.DsReplicaSync(drsuapi_handle, 1, req1) + except Exception, estr: + raise drsException("DsReplicaSync failed %s" % estr) + + +def sendRemoveDsServer(drsuapiBind, drsuapi_handle, server_dsa_dn, domain): + """Send RemoveDSServer request. + + :param drsuapiBind: a drsuapi Bind object + :param drsuapi_handle: a drsuapi hanle on the drsuapi connection + :param server_dsa_dn: a DN object of the server's dsa that we want to + demote + :param domain: a DN object of the server's domain + :raise drsException: if any error occur while sending and receiving the + reply for the DsRemoveDSServer + """ + + try: + req1 = drsuapi.DsRemoveDSServerRequest1() + req1.server_dn = str(server_dsa_dn) + req1.domain_dn = str(domain) + req1.commit = 1 + + drsuapiBind.DsRemoveDSServer(drsuapi_handle, 1, req1) + except Exception, estr: + raise drsException("DsRemoveDSServer failed %s" % estr) + + +def drs_DsBind(drs): + '''make a DsBind call, returning the binding handle''' + bind_info = drsuapi.DsBindInfoCtr() + bind_info.length = 28 + bind_info.info = drsuapi.DsBindInfo28() + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_BASE + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_ASYNC_REPLICATION + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_REMOVEAPI + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_MOVEREQ_V2 + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_GETCHG_COMPRESS + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_DCINFO_V1 + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_RESTORE_USN_OPTIMIZATION + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_KCC_EXECUTE + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_ADDENTRY_V2 + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_LINKED_VALUE_REPLICATION + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_DCINFO_V2 + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_INSTANCE_TYPE_NOT_REQ_ON_MOD + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_CRYPTO_BIND + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_GET_REPL_INFO + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_STRONG_ENCRYPTION + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_DCINFO_V01 + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_TRANSITIVE_MEMBERSHIP + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_ADD_SID_HISTORY + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_POST_BETA3 + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_GET_MEMBERSHIPS2 + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V6 + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_NONDOMAIN_NCS + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V8 + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_GETCHGREPLY_V5 + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_GETCHGREPLY_V6 + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_ADDENTRYREPLY_V3 + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_GETCHGREPLY_V7 + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_VERIFY_OBJECT + (info, handle) = drs.DsBind(misc.GUID(drsuapi.DRSUAPI_DS_BIND_GUID), bind_info) + + return (handle, info.info.supported_extensions) + + +class drs_Replicate(object): + '''DRS replication calls''' + + def __init__(self, binding_string, lp, creds, samdb): + self.drs = drsuapi.drsuapi(binding_string, lp, creds) + (self.drs_handle, self.supported_extensions) = drs_DsBind(self.drs) + self.net = Net(creds=creds, lp=lp) + self.samdb = samdb + self.replication_state = self.net.replicate_init(self.samdb, lp, self.drs) + + def drs_get_rodc_partial_attribute_set(self): + '''get a list of attributes for RODC replication''' + partial_attribute_set = drsuapi.DsPartialAttributeSet() + partial_attribute_set.version = 1 + + attids = [] + + # the exact list of attids we send is quite critical. Note that + # we do ask for the secret attributes, but set SPECIAL_SECRET_PROCESSING + # to zero them out + schema_dn = self.samdb.get_schema_basedn() + res = self.samdb.search(base=schema_dn, scope=ldb.SCOPE_SUBTREE, + expression="objectClass=attributeSchema", + attrs=["lDAPDisplayName", "systemFlags", + "searchFlags"]) + + for r in res: + ldap_display_name = r["lDAPDisplayName"][0] + if "systemFlags" in r: + system_flags = r["systemFlags"][0] + if (int(system_flags) & (samba.dsdb.DS_FLAG_ATTR_NOT_REPLICATED | + samba.dsdb.DS_FLAG_ATTR_IS_CONSTRUCTED)): + continue + if "searchFlags" in r: + search_flags = r["searchFlags"][0] + if (int(search_flags) & samba.dsdb.SEARCH_FLAG_RODC_ATTRIBUTE): + continue + attid = self.samdb.get_attid_from_lDAPDisplayName(ldap_display_name) + attids.append(int(attid)) + + # the attids do need to be sorted, or windows doesn't return + # all the attributes we need + attids.sort() + partial_attribute_set.attids = attids + partial_attribute_set.num_attids = len(attids) + return partial_attribute_set + + def replicate(self, dn, source_dsa_invocation_id, destination_dsa_guid, + schema=False, exop=drsuapi.DRSUAPI_EXOP_NONE, rodc=False, + replica_flags=None): + '''replicate a single DN''' + + # setup for a GetNCChanges call + req8 = drsuapi.DsGetNCChangesRequest8() + + req8.destination_dsa_guid = destination_dsa_guid + req8.source_dsa_invocation_id = source_dsa_invocation_id + req8.naming_context = drsuapi.DsReplicaObjectIdentifier() + req8.naming_context.dn = dn + req8.highwatermark = drsuapi.DsReplicaHighWaterMark() + req8.highwatermark.tmp_highest_usn = 0 + req8.highwatermark.reserved_usn = 0 + req8.highwatermark.highest_usn = 0 + req8.uptodateness_vector = None + if replica_flags is not None: + req8.replica_flags = replica_flags + elif exop == drsuapi.DRSUAPI_EXOP_REPL_SECRET: + req8.replica_flags = 0 + else: + req8.replica_flags = (drsuapi.DRSUAPI_DRS_INIT_SYNC | + drsuapi.DRSUAPI_DRS_PER_SYNC | + drsuapi.DRSUAPI_DRS_GET_ANC | + drsuapi.DRSUAPI_DRS_NEVER_SYNCED) + if rodc: + req8.replica_flags |= drsuapi.DRSUAPI_DRS_SPECIAL_SECRET_PROCESSING + else: + req8.replica_flags |= drsuapi.DRSUAPI_DRS_WRIT_REP + req8.max_object_count = 402 + req8.max_ndr_size = 402116 + req8.extended_op = exop + req8.fsmo_info = 0 + req8.partial_attribute_set = None + req8.partial_attribute_set_ex = None + req8.mapping_ctr.num_mappings = 0 + req8.mapping_ctr.mappings = None + + if not schema and rodc: + req8.partial_attribute_set = self.drs_get_rodc_partial_attribute_set() + + if self.supported_extensions & drsuapi.DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V8: + req_level = 8 + req = req8 + else: + req_level = 5 + req5 = drsuapi.DsGetNCChangesRequest5() + for a in dir(req5): + if a[0] != '_': + setattr(req5, a, getattr(req8, a)) + req = req5 + + while True: + (level, ctr) = self.drs.DsGetNCChanges(self.drs_handle, req_level, req) + if ctr.first_object is None and ctr.object_count != 0: + raise RuntimeError("DsGetNCChanges: NULL first_object with object_count=%u" % (ctr.object_count)) + self.net.replicate_chunk(self.replication_state, level, ctr, + schema=schema, req_level=req_level, req=req) + if ctr.more_data == 0: + break + req.highwatermark = ctr.new_highwatermark diff --git a/python/samba/getopt.py b/python/samba/getopt.py new file mode 100644 index 00000000000..c3c080084ec --- /dev/null +++ b/python/samba/getopt.py @@ -0,0 +1,251 @@ +# Samba-specific bits for optparse +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007 +# +# 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/>. +# + +"""Support for parsing Samba-related command-line options.""" + +__docformat__ = "restructuredText" + +import optparse +import os +from samba.credentials import ( + Credentials, + AUTO_USE_KERBEROS, + DONT_USE_KERBEROS, + MUST_USE_KERBEROS, + ) +from samba.hostconfig import Hostconfig +import sys + + +class SambaOptions(optparse.OptionGroup): + """General Samba-related command line options.""" + + def __init__(self, parser): + from samba.param import LoadParm + optparse.OptionGroup.__init__(self, parser, "Samba Common Options") + self.add_option("-s", "--configfile", action="callback", + type=str, metavar="FILE", help="Configuration file", + callback=self._load_configfile) + self.add_option("-d", "--debuglevel", action="callback", + type=int, metavar="DEBUGLEVEL", help="debug level", + callback=self._set_debuglevel) + self.add_option("--option", action="callback", + type=str, metavar="OPTION", + help="set smb.conf option from command line", + callback=self._set_option) + self.add_option("--realm", action="callback", + type=str, metavar="REALM", help="set the realm name", + callback=self._set_realm) + self._configfile = None + self._lp = LoadParm() + self.realm = None + + def get_loadparm_path(self): + """Return path to the smb.conf file specified on the command line.""" + return self._configfile + + def _load_configfile(self, option, opt_str, arg, parser): + self._configfile = arg + + def _set_debuglevel(self, option, opt_str, arg, parser): + if arg < 0: + raise optparse.OptionValueError("invalid %s option value: %s" % + (opt_str, arg)) + self._lp.set('debug level', str(arg)) + + def _set_realm(self, option, opt_str, arg, parser): + self._lp.set('realm', arg) + self.realm = arg + + def _set_option(self, option, opt_str, arg, parser): + if arg.find('=') == -1: + raise optparse.OptionValueError( + "--option option takes a 'a=b' argument") + a = arg.split('=') + try: + self._lp.set(a[0], a[1]) + except Exception, e: + raise optparse.OptionValueError( + "invalid --option option value %r: %s" % (arg, e)) + + def get_loadparm(self): + """Return loadparm object with data specified on the command line.""" + if self._configfile is not None: + self._lp.load(self._configfile) + elif os.getenv("SMB_CONF_PATH") is not None: + self._lp.load(os.getenv("SMB_CONF_PATH")) + else: + self._lp.load_default() + return self._lp + + def get_hostconfig(self): + return Hostconfig(self.get_loadparm()) + + +class VersionOptions(optparse.OptionGroup): + """Command line option for printing Samba version.""" + def __init__(self, parser): + optparse.OptionGroup.__init__(self, parser, "Version Options") + self.add_option("-V", "--version", action="callback", + callback=self._display_version, + help="Display version number") + + def _display_version(self, option, opt_str, arg, parser): + import samba + print samba.version + sys.exit(0) + + +def parse_kerberos_arg(arg, opt_str): + if arg.lower() in ["yes", 'true', '1']: + return MUST_USE_KERBEROS + elif arg.lower() in ["no", 'false', '0']: + return DONT_USE_KERBEROS + elif arg.lower() in ["auto"]: + return AUTO_USE_KERBEROS + else: + raise optparse.OptionValueError("invalid %s option value: %s" % + (opt_str, arg)) + + +class CredentialsOptions(optparse.OptionGroup): + """Command line options for specifying credentials.""" + + def __init__(self, parser): + self.no_pass = True + self.ipaddress = None + optparse.OptionGroup.__init__(self, parser, "Credentials Options") + self.add_option("--simple-bind-dn", metavar="DN", action="callback", + callback=self._set_simple_bind_dn, type=str, + help="DN to use for a simple bind") + self.add_option("--password", metavar="PASSWORD", action="callback", + help="Password", type=str, callback=self._set_password) + self.add_option("-U", "--username", metavar="USERNAME", + action="callback", type=str, + help="Username", callback=self._parse_username) + self.add_option("-W", "--workgroup", metavar="WORKGROUP", + action="callback", type=str, + help="Workgroup", callback=self._parse_workgroup) + self.add_option("-N", "--no-pass", action="store_true", + help="Don't ask for a password") + self.add_option("-k", "--kerberos", metavar="KERBEROS", + action="callback", type=str, + help="Use Kerberos", callback=self._set_kerberos) + self.add_option("", "--ipaddress", metavar="IPADDRESS", + action="callback", type=str, + help="IP address of server", + callback=self._set_ipaddress) + self.creds = Credentials() + + def _parse_username(self, option, opt_str, arg, parser): + self.creds.parse_string(arg) + + def _parse_workgroup(self, option, opt_str, arg, parser): + self.creds.set_domain(arg) + + def _set_password(self, option, opt_str, arg, parser): + self.creds.set_password(arg) + self.no_pass = False + + def _set_ipaddress(self, option, opt_str, arg, parser): + self.ipaddress = arg + + def _set_kerberos(self, option, opt_str, arg, parser): + self.creds.set_kerberos_state(parse_kerberos_arg(arg, opt_str)) + + def _set_simple_bind_dn(self, option, opt_str, arg, parser): + self.creds.set_bind_dn(arg) + + def get_credentials(self, lp, fallback_machine=False): + """Obtain the credentials set on the command-line. + + :param lp: Loadparm object to use. + :return: Credentials object + """ + self.creds.guess(lp) + if self.no_pass: + self.creds.set_cmdline_callbacks() + + # possibly fallback to using the machine account, if we have + # access to the secrets db + if fallback_machine and not self.creds.authentication_requested(): + try: + self.creds.set_machine_account(lp) + except Exception: + pass + + return self.creds + + +class CredentialsOptionsDouble(CredentialsOptions): + """Command line options for specifying credentials of two servers.""" + + def __init__(self, parser): + CredentialsOptions.__init__(self, parser) + self.no_pass2 = True + self.add_option("--simple-bind-dn2", metavar="DN2", action="callback", + callback=self._set_simple_bind_dn2, type=str, + help="DN to use for a simple bind") + self.add_option("--password2", metavar="PASSWORD2", action="callback", + help="Password", type=str, + callback=self._set_password2) + self.add_option("--username2", metavar="USERNAME2", + action="callback", type=str, + help="Username for second server", + callback=self._parse_username2) + self.add_option("--workgroup2", metavar="WORKGROUP2", + action="callback", type=str, + help="Workgroup for second server", + callback=self._parse_workgroup2) + self.add_option("--no-pass2", action="store_true", + help="Don't ask for a password for the second server") + self.add_option("--kerberos2", metavar="KERBEROS2", + action="callback", type=str, + help="Use Kerberos", callback=self._set_kerberos2) + self.creds2 = Credentials() + + def _parse_username2(self, option, opt_str, arg, parser): + self.creds2.parse_string(arg) + + def _parse_workgroup2(self, option, opt_str, arg, parser): + self.creds2.set_domain(arg) + + def _set_password2(self, option, opt_str, arg, parser): + self.creds2.set_password(arg) + self.no_pass2 = False + + def _set_kerberos2(self, option, opt_str, arg, parser): + self.creds2.set_kerberos_state(parse_kerberos_arg(arg, opt_str)) + + def _set_simple_bind_dn2(self, option, opt_str, arg, parser): + self.creds2.set_bind_dn(arg) + + def get_credentials2(self, lp, guess=True): + """Obtain the credentials set on the command-line. + + :param lp: Loadparm object to use. + :param guess: Try guess Credentials from environment + :return: Credentials object + """ + if guess: + self.creds2.guess(lp) + elif not self.creds2.get_username(): + self.creds2.set_anonymous() + + if self.no_pass2: + self.creds2.set_cmdline_callbacks() + return self.creds2 diff --git a/python/samba/hostconfig.py b/python/samba/hostconfig.py new file mode 100644 index 00000000000..a66fbc23130 --- /dev/null +++ b/python/samba/hostconfig.py @@ -0,0 +1,81 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2008 +# +# 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/>. +# + +"""Local host configuration.""" + +from samdb import SamDB + +class Hostconfig(object): + """Aggregate object that contains all information about the configuration + of a Samba host.""" + + def __init__(self, lp): + self.lp = lp + + def get_shares(self): + return SharesContainer(self.lp) + + def get_samdb(self, session_info, credentials): + """Access the SamDB host. + + :param session_info: Session info to use + :param credentials: Credentials to access the SamDB with + """ + return SamDB(url=self.lp.samdb_url(), + session_info=session_info, credentials=credentials, + lp=self.lp) + + +# TODO: Rather than accessing Loadparm directly here, we should really +# have bindings to the param/shares.c and use those. + + +class SharesContainer(object): + """A shares container.""" + + def __init__(self, lp): + self._lp = lp + + def __getitem__(self, name): + if name == "global": + # [global] is not a share + raise KeyError + return Share(self._lp[name]) + + def __len__(self): + if "global" in self._lp.services(): + return len(self._lp)-1 + return len(self._lp) + + def keys(self): + return [name for name in self._lp.services() if name != "global"] + + def __iter__(self): + return iter(self.keys()) + + +class Share(object): + """A file share.""" + + def __init__(self, service): + self._service = service + + def __getitem__(self, name): + return self._service[name] + + def __setitem__(self, name, value): + self._service[name] = value diff --git a/python/samba/idmap.py b/python/samba/idmap.py new file mode 100644 index 00000000000..0cb729fbc29 --- /dev/null +++ b/python/samba/idmap.py @@ -0,0 +1,98 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) 2008 Kai Blin <kai@samba.org> +# +# +# 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/>. +# + +"""Convenience functions for using the idmap database.""" + +__docformat__ = "restructuredText" + +import ldb +import samba + +class IDmapDB(samba.Ldb): + """The IDmap database.""" + + # Mappings for ID_TYPE_UID, ID_TYPE_GID and ID_TYPE_BOTH + TYPE_UID = 1 + TYPE_GID = 2 + TYPE_BOTH = 3 + + def __init__(self, url=None, lp=None, modules_dir=None, session_info=None, + credentials=None, flags=0, options=None): + """Opens the IDMap Database. + + For parameter meanings see the super class (samba.Ldb) + """ + self.lp = lp + if url is None: + url = lp.private_path("idmap.ldb") + + super(IDmapDB, self).__init__(url=url, lp=lp, modules_dir=modules_dir, + session_info=session_info, credentials=credentials, flags=flags, + options=options) + + def connect(self, url=None, flags=0, options=None): + super(IDmapDB, self).connect(url=self.lp.private_path(url), flags=flags, + options=options) + + def increment_xid(self): + """Increment xidNumber, if not present it create and assign it to the lowerBound + + :return xid can that be used for SID/unixid mapping + """ + res = self.search(expression="distinguishedName=CN=CONFIG", base="", + scope=ldb.SCOPE_SUBTREE) + id = res[0].get("xidNumber") + flag = ldb.FLAG_MOD_REPLACE + if id is None: + id = res[0].get("lowerBound") + flag = ldb.FLAG_MOD_ADD + newid = int(str(id)) + 1 + msg = ldb.Message() + msg.dn = ldb.Dn(self, "CN=CONFIG") + msg["xidNumber"] = ldb.MessageElement(str(newid), flag, "xidNumber") + self.modify(msg) + return id + + def setup_name_mapping(self, sid, type, unixid=None): + """Setup a mapping between a sam name and a unix name. + + :param sid: SID of the NT-side of the mapping. + :param unixname: Unix id to map to, if none supplied the next one will be selected + """ + if unixid is None: + unixid = self.increment_xid() + type_string = "" + if type == self.TYPE_UID: + type_string = "ID_TYPE_UID" + elif type == self.TYPE_GID: + type_string = "ID_TYPE_GID" + elif type == self.TYPE_BOTH: + type_string = "ID_TYPE_BOTH" + else: + return + + mod = """ +dn: CN=%s +xidNumber: %s +objectSid: %s +objectClass: sidMap +type: %s +cn: %s + +""" % (sid, unixid, sid, type_string, sid) + self.add(self.parse_ldif(mod).next()[1]) diff --git a/python/samba/join.py b/python/samba/join.py new file mode 100644 index 00000000000..c55c22cad53 --- /dev/null +++ b/python/samba/join.py @@ -0,0 +1,1149 @@ +# python join code +# Copyright Andrew Tridgell 2010 +# Copyright Andrew Bartlett 2010 +# +# 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/>. +# + +"""Joining a domain.""" + +from samba.auth import system_session +from samba.samdb import SamDB +from samba import gensec, Ldb, drs_utils +import ldb, samba, sys, uuid +from samba.ndr import ndr_pack +from samba.dcerpc import security, drsuapi, misc, nbt, lsa, drsblobs +from samba.credentials import Credentials, DONT_USE_KERBEROS +from samba.provision import secretsdb_self_join, provision, provision_fill, FILL_DRS, FILL_SUBDOMAIN +from samba.schema import Schema +from samba.net import Net +from samba.provision.sambadns import setup_bind9_dns +import logging +import talloc +import random +import time + +# this makes debugging easier +talloc.enable_null_tracking() + +class DCJoinException(Exception): + + def __init__(self, msg): + super(DCJoinException, self).__init__("Can't join, error: %s" % msg) + + +class dc_join(object): + """Perform a DC join.""" + + def __init__(ctx, server=None, creds=None, lp=None, site=None, + netbios_name=None, targetdir=None, domain=None, + machinepass=None, use_ntvfs=False, dns_backend=None, + promote_existing=False): + ctx.creds = creds + ctx.lp = lp + ctx.site = site + ctx.netbios_name = netbios_name + ctx.targetdir = targetdir + ctx.use_ntvfs = use_ntvfs + + ctx.promote_existing = promote_existing + ctx.promote_from_dn = None + + ctx.nc_list = [] + ctx.full_nc_list = [] + + ctx.creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL) + ctx.net = Net(creds=ctx.creds, lp=ctx.lp) + + if server is not None: + ctx.server = server + else: + print("Finding a writeable DC for domain '%s'" % domain) + ctx.server = ctx.find_dc(domain) + print("Found DC %s" % ctx.server) + + ctx.samdb = SamDB(url="ldap://%s" % ctx.server, + session_info=system_session(), + credentials=ctx.creds, lp=ctx.lp) + + try: + ctx.samdb.search(scope=ldb.SCOPE_ONELEVEL, attrs=["dn"]) + except ldb.LdbError, (enum, estr): + raise DCJoinException(estr) + + + ctx.myname = netbios_name + ctx.samname = "%s$" % ctx.myname + ctx.base_dn = str(ctx.samdb.get_default_basedn()) + ctx.root_dn = str(ctx.samdb.get_root_basedn()) + ctx.schema_dn = str(ctx.samdb.get_schema_basedn()) + ctx.config_dn = str(ctx.samdb.get_config_basedn()) + ctx.domsid = ctx.samdb.get_domain_sid() + ctx.domain_name = ctx.get_domain_name() + ctx.forest_domain_name = ctx.get_forest_domain_name() + ctx.invocation_id = misc.GUID(str(uuid.uuid4())) + + ctx.dc_ntds_dn = ctx.samdb.get_dsServiceName() + ctx.dc_dnsHostName = ctx.get_dnsHostName() + ctx.behavior_version = ctx.get_behavior_version() + + if machinepass is not None: + ctx.acct_pass = machinepass + else: + ctx.acct_pass = samba.generate_random_password(32, 40) + + # work out the DNs of all the objects we will be adding + ctx.server_dn = "CN=%s,CN=Servers,CN=%s,CN=Sites,%s" % (ctx.myname, ctx.site, ctx.config_dn) + ctx.ntds_dn = "CN=NTDS Settings,%s" % ctx.server_dn + topology_base = "CN=Topology,CN=Domain System Volume,CN=DFSR-GlobalSettings,CN=System,%s" % ctx.base_dn + if ctx.dn_exists(topology_base): + ctx.topology_dn = "CN=%s,%s" % (ctx.myname, topology_base) + else: + ctx.topology_dn = None + + ctx.dnsdomain = ctx.samdb.domain_dns_name() + ctx.dnsforest = ctx.samdb.forest_dns_name() + ctx.domaindns_zone = 'DC=DomainDnsZones,%s' % ctx.base_dn + ctx.forestdns_zone = 'DC=ForestDnsZones,%s' % ctx.base_dn + + res_domaindns = ctx.samdb.search(scope=ldb.SCOPE_ONELEVEL, + attrs=[], + base=ctx.samdb.get_partitions_dn(), + expression="(&(objectClass=crossRef)(ncName=%s))" % ctx.domaindns_zone) + if dns_backend is None: + ctx.dns_backend = "NONE" + else: + if len(res_domaindns) == 0: + ctx.dns_backend = "NONE" + print "NO DNS zone information found in source domain, not replicating DNS" + else: + ctx.dns_backend = dns_backend + + ctx.dnshostname = "%s.%s" % (ctx.myname, ctx.dnsdomain) + + ctx.realm = ctx.dnsdomain + + ctx.acct_dn = "CN=%s,OU=Domain Controllers,%s" % (ctx.myname, ctx.base_dn) + + ctx.tmp_samdb = None + + ctx.SPNs = [ "HOST/%s" % ctx.myname, + "HOST/%s" % ctx.dnshostname, + "GC/%s/%s" % (ctx.dnshostname, ctx.dnsforest) ] + + # these elements are optional + ctx.never_reveal_sid = None + ctx.reveal_sid = None + ctx.connection_dn = None + ctx.RODC = False + ctx.krbtgt_dn = None + ctx.drsuapi = None + ctx.managedby = None + ctx.subdomain = False + + def del_noerror(ctx, dn, recursive=False): + if recursive: + try: + res = ctx.samdb.search(base=dn, scope=ldb.SCOPE_ONELEVEL, attrs=["dn"]) + except Exception: + return + for r in res: + ctx.del_noerror(r.dn, recursive=True) + try: + ctx.samdb.delete(dn) + print "Deleted %s" % dn + except Exception: + pass + + def cleanup_old_join(ctx): + """Remove any DNs from a previous join.""" + try: + # find the krbtgt link + print("checking sAMAccountName") + if ctx.subdomain: + res = None + else: + res = ctx.samdb.search(base=ctx.samdb.get_default_basedn(), + expression='sAMAccountName=%s' % ldb.binary_encode(ctx.samname), + attrs=["msDS-krbTgtLink"]) + if res: + ctx.del_noerror(res[0].dn, recursive=True) + if ctx.connection_dn is not None: + ctx.del_noerror(ctx.connection_dn) + if ctx.krbtgt_dn is not None: + ctx.del_noerror(ctx.krbtgt_dn) + ctx.del_noerror(ctx.ntds_dn) + ctx.del_noerror(ctx.server_dn, recursive=True) + if ctx.topology_dn: + ctx.del_noerror(ctx.topology_dn) + if ctx.partition_dn: + ctx.del_noerror(ctx.partition_dn) + if res: + ctx.new_krbtgt_dn = res[0]["msDS-Krbtgtlink"][0] + ctx.del_noerror(ctx.new_krbtgt_dn) + + if ctx.subdomain: + binding_options = "sign" + lsaconn = lsa.lsarpc("ncacn_ip_tcp:%s[%s]" % (ctx.server, binding_options), + ctx.lp, ctx.creds) + + objectAttr = lsa.ObjectAttribute() + objectAttr.sec_qos = lsa.QosInfo() + + pol_handle = lsaconn.OpenPolicy2(''.decode('utf-8'), + objectAttr, security.SEC_FLAG_MAXIMUM_ALLOWED) + + name = lsa.String() + name.string = ctx.realm + info = lsaconn.QueryTrustedDomainInfoByName(pol_handle, name, lsa.LSA_TRUSTED_DOMAIN_INFO_FULL_INFO) + + lsaconn.DeleteTrustedDomain(pol_handle, info.info_ex.sid) + + name = lsa.String() + name.string = ctx.forest_domain_name + info = lsaconn.QueryTrustedDomainInfoByName(pol_handle, name, lsa.LSA_TRUSTED_DOMAIN_INFO_FULL_INFO) + + lsaconn.DeleteTrustedDomain(pol_handle, info.info_ex.sid) + + except Exception: + pass + + def promote_possible(ctx): + """confirm that the account is just a bare NT4 BDC or a member server, so can be safely promoted""" + if ctx.subdomain: + # This shouldn't happen + raise Exception("Can not promote into a subdomain") + + res = ctx.samdb.search(base=ctx.samdb.get_default_basedn(), + expression='sAMAccountName=%s' % ldb.binary_encode(ctx.samname), + attrs=["msDS-krbTgtLink", "userAccountControl", "serverReferenceBL", "rIDSetReferences"]) + if len(res) == 0: + raise Exception("Could not find domain member account '%s' to promote to a DC, use 'samba-tool domain join' instead'" % ctx.samname) + if "msDS-krbTgtLink" in res[0] or "serverReferenceBL" in res[0] or "rIDSetReferences" in res[0]: + raise Exception("Account '%s' appears to be an active DC, use 'samba-tool domain join' if you must re-create this account" % ctx.samname) + if (int(res[0]["userAccountControl"][0]) & (samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT|samba.dsdb.UF_SERVER_TRUST_ACCOUNT) == 0): + raise Exception("Account %s is not a domain member or a bare NT4 BDC, use 'samba-tool domain join' instead'" % ctx.samname) + + ctx.promote_from_dn = res[0].dn + + + def find_dc(ctx, domain): + """find a writeable DC for the given domain""" + try: + ctx.cldap_ret = ctx.net.finddc(domain=domain, flags=nbt.NBT_SERVER_LDAP | nbt.NBT_SERVER_DS | nbt.NBT_SERVER_WRITABLE) + except Exception: + raise Exception("Failed to find a writeable DC for domain '%s'" % domain) + if ctx.cldap_ret.client_site is not None and ctx.cldap_ret.client_site != "": + ctx.site = ctx.cldap_ret.client_site + return ctx.cldap_ret.pdc_dns_name + + + def get_behavior_version(ctx): + res = ctx.samdb.search(base=ctx.base_dn, scope=ldb.SCOPE_BASE, attrs=["msDS-Behavior-Version"]) + if "msDS-Behavior-Version" in res[0]: + return int(res[0]["msDS-Behavior-Version"][0]) + else: + return samba.dsdb.DS_DOMAIN_FUNCTION_2000 + + def get_dnsHostName(ctx): + res = ctx.samdb.search(base="", scope=ldb.SCOPE_BASE, attrs=["dnsHostName"]) + return res[0]["dnsHostName"][0] + + def get_domain_name(ctx): + '''get netbios name of the domain from the partitions record''' + partitions_dn = ctx.samdb.get_partitions_dn() + res = ctx.samdb.search(base=partitions_dn, scope=ldb.SCOPE_ONELEVEL, attrs=["nETBIOSName"], + expression='ncName=%s' % ctx.samdb.get_default_basedn()) + return res[0]["nETBIOSName"][0] + + def get_forest_domain_name(ctx): + '''get netbios name of the domain from the partitions record''' + partitions_dn = ctx.samdb.get_partitions_dn() + res = ctx.samdb.search(base=partitions_dn, scope=ldb.SCOPE_ONELEVEL, attrs=["nETBIOSName"], + expression='ncName=%s' % ctx.samdb.get_root_basedn()) + return res[0]["nETBIOSName"][0] + + def get_parent_partition_dn(ctx): + '''get the parent domain partition DN from parent DNS name''' + res = ctx.samdb.search(base=ctx.config_dn, attrs=[], + expression='(&(objectclass=crossRef)(dnsRoot=%s)(systemFlags:%s:=%u))' % + (ctx.parent_dnsdomain, ldb.OID_COMPARATOR_AND, samba.dsdb.SYSTEM_FLAG_CR_NTDS_DOMAIN)) + return str(res[0].dn) + + def get_naming_master(ctx): + '''get the parent domain partition DN from parent DNS name''' + res = ctx.samdb.search(base='CN=Partitions,%s' % ctx.config_dn, attrs=['fSMORoleOwner'], + scope=ldb.SCOPE_BASE, controls=["extended_dn:1:1"]) + if not 'fSMORoleOwner' in res[0]: + raise DCJoinException("Can't find naming master on partition DN %s" % ctx.partition_dn) + master_guid = str(misc.GUID(ldb.Dn(ctx.samdb, res[0]['fSMORoleOwner'][0]).get_extended_component('GUID'))) + master_host = '%s._msdcs.%s' % (master_guid, ctx.dnsforest) + return master_host + + def get_mysid(ctx): + '''get the SID of the connected user. Only works with w2k8 and later, + so only used for RODC join''' + res = ctx.samdb.search(base="", scope=ldb.SCOPE_BASE, attrs=["tokenGroups"]) + binsid = res[0]["tokenGroups"][0] + return ctx.samdb.schema_format_value("objectSID", binsid) + + def dn_exists(ctx, dn): + '''check if a DN exists''' + try: + res = ctx.samdb.search(base=dn, scope=ldb.SCOPE_BASE, attrs=[]) + except ldb.LdbError, (enum, estr): + if enum == ldb.ERR_NO_SUCH_OBJECT: + return False + raise + return True + + def add_krbtgt_account(ctx): + '''RODCs need a special krbtgt account''' + print "Adding %s" % ctx.krbtgt_dn + rec = { + "dn" : ctx.krbtgt_dn, + "objectclass" : "user", + "useraccountcontrol" : str(samba.dsdb.UF_NORMAL_ACCOUNT | + samba.dsdb.UF_ACCOUNTDISABLE), + "showinadvancedviewonly" : "TRUE", + "description" : "krbtgt for %s" % ctx.samname} + ctx.samdb.add(rec, ["rodc_join:1:1"]) + + # now we need to search for the samAccountName attribute on the krbtgt DN, + # as this will have been magically set to the krbtgt number + res = ctx.samdb.search(base=ctx.krbtgt_dn, scope=ldb.SCOPE_BASE, attrs=["samAccountName"]) + ctx.krbtgt_name = res[0]["samAccountName"][0] + + print "Got krbtgt_name=%s" % ctx.krbtgt_name + + m = ldb.Message() + m.dn = ldb.Dn(ctx.samdb, ctx.acct_dn) + m["msDS-krbTgtLink"] = ldb.MessageElement(ctx.krbtgt_dn, + ldb.FLAG_MOD_REPLACE, "msDS-krbTgtLink") + ctx.samdb.modify(m) + + ctx.new_krbtgt_dn = "CN=%s,CN=Users,%s" % (ctx.krbtgt_name, ctx.base_dn) + print "Renaming %s to %s" % (ctx.krbtgt_dn, ctx.new_krbtgt_dn) + ctx.samdb.rename(ctx.krbtgt_dn, ctx.new_krbtgt_dn) + + def drsuapi_connect(ctx): + '''make a DRSUAPI connection to the naming master''' + binding_options = "seal" + if int(ctx.lp.get("log level")) >= 4: + binding_options += ",print" + binding_string = "ncacn_ip_tcp:%s[%s]" % (ctx.server, binding_options) + ctx.drsuapi = drsuapi.drsuapi(binding_string, ctx.lp, ctx.creds) + (ctx.drsuapi_handle, ctx.bind_supported_extensions) = drs_utils.drs_DsBind(ctx.drsuapi) + + def create_tmp_samdb(ctx): + '''create a temporary samdb object for schema queries''' + ctx.tmp_schema = Schema(security.dom_sid(ctx.domsid), + schemadn=ctx.schema_dn) + ctx.tmp_samdb = SamDB(session_info=system_session(), url=None, auto_connect=False, + credentials=ctx.creds, lp=ctx.lp, global_schema=False, + am_rodc=False) + ctx.tmp_samdb.set_schema(ctx.tmp_schema) + + def build_DsReplicaAttribute(ctx, attrname, attrvalue): + '''build a DsReplicaAttributeCtr object''' + r = drsuapi.DsReplicaAttribute() + r.attid = ctx.tmp_samdb.get_attid_from_lDAPDisplayName(attrname) + r.value_ctr = 1 + + + def DsAddEntry(ctx, recs): + '''add a record via the DRSUAPI DsAddEntry call''' + if ctx.drsuapi is None: + ctx.drsuapi_connect() + if ctx.tmp_samdb is None: + ctx.create_tmp_samdb() + + objects = [] + for rec in recs: + id = drsuapi.DsReplicaObjectIdentifier() + id.dn = rec['dn'] + + attrs = [] + for a in rec: + if a == 'dn': + continue + if not isinstance(rec[a], list): + v = [rec[a]] + else: + v = rec[a] + rattr = ctx.tmp_samdb.dsdb_DsReplicaAttribute(ctx.tmp_samdb, a, v) + attrs.append(rattr) + + attribute_ctr = drsuapi.DsReplicaAttributeCtr() + attribute_ctr.num_attributes = len(attrs) + attribute_ctr.attributes = attrs + + object = drsuapi.DsReplicaObject() + object.identifier = id + object.attribute_ctr = attribute_ctr + + list_object = drsuapi.DsReplicaObjectListItem() + list_object.object = object + objects.append(list_object) + + req2 = drsuapi.DsAddEntryRequest2() + req2.first_object = objects[0] + prev = req2.first_object + for o in objects[1:]: + prev.next_object = o + prev = o + + (level, ctr) = ctx.drsuapi.DsAddEntry(ctx.drsuapi_handle, 2, req2) + if level == 2: + if ctr.dir_err != drsuapi.DRSUAPI_DIRERR_OK: + print("DsAddEntry failed with dir_err %u" % ctr.dir_err) + raise RuntimeError("DsAddEntry failed") + if ctr.extended_err != (0, 'WERR_OK'): + print("DsAddEntry failed with status %s info %s" % (ctr.extended_err)) + raise RuntimeError("DsAddEntry failed") + if level == 3: + if ctr.err_ver != 1: + raise RuntimeError("expected err_ver 1, got %u" % ctr.err_ver) + if ctr.err_data.status != (0, 'WERR_OK'): + print("DsAddEntry failed with status %s info %s" % (ctr.err_data.status, + ctr.err_data.info.extended_err)) + raise RuntimeError("DsAddEntry failed") + if ctr.err_data.dir_err != drsuapi.DRSUAPI_DIRERR_OK: + print("DsAddEntry failed with dir_err %u" % ctr.err_data.dir_err) + raise RuntimeError("DsAddEntry failed") + + return ctr.objects + + def join_add_ntdsdsa(ctx): + '''add the ntdsdsa object''' + + print "Adding %s" % ctx.ntds_dn + rec = { + "dn" : ctx.ntds_dn, + "objectclass" : "nTDSDSA", + "systemFlags" : str(samba.dsdb.SYSTEM_FLAG_DISALLOW_MOVE_ON_DELETE), + "dMDLocation" : ctx.schema_dn} + + nc_list = [ ctx.base_dn, ctx.config_dn, ctx.schema_dn ] + + if ctx.behavior_version >= samba.dsdb.DS_DOMAIN_FUNCTION_2003: + rec["msDS-Behavior-Version"] = str(samba.dsdb.DS_DOMAIN_FUNCTION_2008_R2) + + if ctx.behavior_version >= samba.dsdb.DS_DOMAIN_FUNCTION_2003: + rec["msDS-HasDomainNCs"] = ctx.base_dn + + if ctx.RODC: + rec["objectCategory"] = "CN=NTDS-DSA-RO,%s" % ctx.schema_dn + rec["msDS-HasFullReplicaNCs"] = ctx.nc_list + rec["options"] = "37" + ctx.samdb.add(rec, ["rodc_join:1:1"]) + else: + rec["objectCategory"] = "CN=NTDS-DSA,%s" % ctx.schema_dn + rec["HasMasterNCs"] = nc_list + if ctx.behavior_version >= samba.dsdb.DS_DOMAIN_FUNCTION_2003: + rec["msDS-HasMasterNCs"] = ctx.nc_list + rec["options"] = "1" + rec["invocationId"] = ndr_pack(ctx.invocation_id) + ctx.DsAddEntry([rec]) + + # find the GUID of our NTDS DN + res = ctx.samdb.search(base=ctx.ntds_dn, scope=ldb.SCOPE_BASE, attrs=["objectGUID"]) + ctx.ntds_guid = misc.GUID(ctx.samdb.schema_format_value("objectGUID", res[0]["objectGUID"][0])) + + def join_add_objects(ctx): + '''add the various objects needed for the join''' + if ctx.acct_dn: + print "Adding %s" % ctx.acct_dn + rec = { + "dn" : ctx.acct_dn, + "objectClass": "computer", + "displayname": ctx.samname, + "samaccountname" : ctx.samname, + "userAccountControl" : str(ctx.userAccountControl | samba.dsdb.UF_ACCOUNTDISABLE), + "dnshostname" : ctx.dnshostname} + if ctx.behavior_version >= samba.dsdb.DS_DOMAIN_FUNCTION_2008: + rec['msDS-SupportedEncryptionTypes'] = str(samba.dsdb.ENC_ALL_TYPES) + elif ctx.promote_existing: + rec['msDS-SupportedEncryptionTypes'] = [] + if ctx.managedby: + rec["managedby"] = ctx.managedby + elif ctx.promote_existing: + rec["managedby"] = [] + + if ctx.never_reveal_sid: + rec["msDS-NeverRevealGroup"] = ctx.never_reveal_sid + elif ctx.promote_existing: + rec["msDS-NeverRevealGroup"] = [] + + if ctx.reveal_sid: + rec["msDS-RevealOnDemandGroup"] = ctx.reveal_sid + elif ctx.promote_existing: + rec["msDS-RevealOnDemandGroup"] = [] + + if ctx.promote_existing: + if ctx.promote_from_dn != ctx.acct_dn: + ctx.samdb.rename(ctx.promote_from_dn, ctx.acct_dn) + ctx.samdb.modify(ldb.Message.from_dict(ctx.samdb, rec, ldb.FLAG_MOD_REPLACE)) + else: + ctx.samdb.add(rec) + + if ctx.krbtgt_dn: + ctx.add_krbtgt_account() + + print "Adding %s" % ctx.server_dn + rec = { + "dn": ctx.server_dn, + "objectclass" : "server", + # windows uses 50000000 decimal for systemFlags. A windows hex/decimal mixup bug? + "systemFlags" : str(samba.dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME | + samba.dsdb.SYSTEM_FLAG_CONFIG_ALLOW_LIMITED_MOVE | + samba.dsdb.SYSTEM_FLAG_DISALLOW_MOVE_ON_DELETE), + # windows seems to add the dnsHostName later + "dnsHostName" : ctx.dnshostname} + + if ctx.acct_dn: + rec["serverReference"] = ctx.acct_dn + + ctx.samdb.add(rec) + + if ctx.subdomain: + # the rest is done after replication + ctx.ntds_guid = None + return + + ctx.join_add_ntdsdsa() + + if ctx.connection_dn is not None: + print "Adding %s" % ctx.connection_dn + rec = { + "dn" : ctx.connection_dn, + "objectclass" : "nTDSConnection", + "enabledconnection" : "TRUE", + "options" : "65", + "fromServer" : ctx.dc_ntds_dn} + ctx.samdb.add(rec) + + if ctx.acct_dn: + print "Adding SPNs to %s" % ctx.acct_dn + m = ldb.Message() + m.dn = ldb.Dn(ctx.samdb, ctx.acct_dn) + for i in range(len(ctx.SPNs)): + ctx.SPNs[i] = ctx.SPNs[i].replace("$NTDSGUID", str(ctx.ntds_guid)) + m["servicePrincipalName"] = ldb.MessageElement(ctx.SPNs, + ldb.FLAG_MOD_REPLACE, + "servicePrincipalName") + ctx.samdb.modify(m) + + # The account password set operation should normally be done over + # LDAP. Windows 2000 DCs however allow this only with SSL + # connections which are hard to set up and otherwise refuse with + # ERR_UNWILLING_TO_PERFORM. In this case we fall back to libnet + # over SAMR. + print "Setting account password for %s" % ctx.samname + try: + ctx.samdb.setpassword("(&(objectClass=user)(sAMAccountName=%s))" + % ldb.binary_encode(ctx.samname), + ctx.acct_pass, + force_change_at_next_login=False, + username=ctx.samname) + except ldb.LdbError, (num, _): + if num != ldb.ERR_UNWILLING_TO_PERFORM: + pass + ctx.net.set_password(account_name=ctx.samname, + domain_name=ctx.domain_name, + newpassword=ctx.acct_pass) + + res = ctx.samdb.search(base=ctx.acct_dn, scope=ldb.SCOPE_BASE, + attrs=["msDS-KeyVersionNumber"]) + if "msDS-KeyVersionNumber" in res[0]: + ctx.key_version_number = int(res[0]["msDS-KeyVersionNumber"][0]) + else: + ctx.key_version_number = None + + print("Enabling account") + m = ldb.Message() + m.dn = ldb.Dn(ctx.samdb, ctx.acct_dn) + m["userAccountControl"] = ldb.MessageElement(str(ctx.userAccountControl), + ldb.FLAG_MOD_REPLACE, + "userAccountControl") + ctx.samdb.modify(m) + + def join_add_objects2(ctx): + """add the various objects needed for the join, for subdomains post replication""" + + print "Adding %s" % ctx.partition_dn + # NOTE: windows sends a ntSecurityDescriptor here, we + # let it default + rec = { + "dn" : ctx.partition_dn, + "objectclass" : "crossRef", + "objectCategory" : "CN=Cross-Ref,%s" % ctx.schema_dn, + "nCName" : ctx.base_dn, + "nETBIOSName" : ctx.domain_name, + "dnsRoot": ctx.dnsdomain, + "trustParent" : ctx.parent_partition_dn, + "systemFlags" : str(samba.dsdb.SYSTEM_FLAG_CR_NTDS_NC|samba.dsdb.SYSTEM_FLAG_CR_NTDS_DOMAIN)} + if ctx.behavior_version >= samba.dsdb.DS_DOMAIN_FUNCTION_2003: + rec["msDS-Behavior-Version"] = str(ctx.behavior_version) + + rec2 = { + "dn" : ctx.ntds_dn, + "objectclass" : "nTDSDSA", + "systemFlags" : str(samba.dsdb.SYSTEM_FLAG_DISALLOW_MOVE_ON_DELETE), + "dMDLocation" : ctx.schema_dn} + + nc_list = [ ctx.base_dn, ctx.config_dn, ctx.schema_dn ] + + if ctx.behavior_version >= samba.dsdb.DS_DOMAIN_FUNCTION_2003: + rec2["msDS-Behavior-Version"] = str(ctx.behavior_version) + + if ctx.behavior_version >= samba.dsdb.DS_DOMAIN_FUNCTION_2003: + rec2["msDS-HasDomainNCs"] = ctx.base_dn + + rec2["objectCategory"] = "CN=NTDS-DSA,%s" % ctx.schema_dn + rec2["HasMasterNCs"] = nc_list + if ctx.behavior_version >= samba.dsdb.DS_DOMAIN_FUNCTION_2003: + rec2["msDS-HasMasterNCs"] = ctx.nc_list + rec2["options"] = "1" + rec2["invocationId"] = ndr_pack(ctx.invocation_id) + + objects = ctx.DsAddEntry([rec, rec2]) + if len(objects) != 2: + raise DCJoinException("Expected 2 objects from DsAddEntry") + + ctx.ntds_guid = objects[1].guid + + print("Replicating partition DN") + ctx.repl.replicate(ctx.partition_dn, + misc.GUID("00000000-0000-0000-0000-000000000000"), + ctx.ntds_guid, + exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ, + replica_flags=drsuapi.DRSUAPI_DRS_WRIT_REP) + + print("Replicating NTDS DN") + ctx.repl.replicate(ctx.ntds_dn, + misc.GUID("00000000-0000-0000-0000-000000000000"), + ctx.ntds_guid, + exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ, + replica_flags=drsuapi.DRSUAPI_DRS_WRIT_REP) + + def join_provision(ctx): + """Provision the local SAM.""" + + print "Calling bare provision" + + logger = logging.getLogger("provision") + logger.addHandler(logging.StreamHandler(sys.stdout)) + smbconf = ctx.lp.configfile + + presult = provision(logger, system_session(), None, smbconf=smbconf, + targetdir=ctx.targetdir, samdb_fill=FILL_DRS, realm=ctx.realm, + rootdn=ctx.root_dn, domaindn=ctx.base_dn, + schemadn=ctx.schema_dn, configdn=ctx.config_dn, + serverdn=ctx.server_dn, domain=ctx.domain_name, + hostname=ctx.myname, domainsid=ctx.domsid, + machinepass=ctx.acct_pass, serverrole="domain controller", + sitename=ctx.site, lp=ctx.lp, ntdsguid=ctx.ntds_guid, + use_ntvfs=ctx.use_ntvfs, dns_backend=ctx.dns_backend) + print "Provision OK for domain DN %s" % presult.domaindn + ctx.local_samdb = presult.samdb + ctx.lp = presult.lp + ctx.paths = presult.paths + ctx.names = presult.names + + def join_provision_own_domain(ctx): + """Provision the local SAM.""" + + # we now operate exclusively on the local database, which + # we need to reopen in order to get the newly created schema + print("Reconnecting to local samdb") + ctx.samdb = SamDB(url=ctx.local_samdb.url, + session_info=system_session(), + lp=ctx.local_samdb.lp, + global_schema=False) + ctx.samdb.set_invocation_id(str(ctx.invocation_id)) + ctx.local_samdb = ctx.samdb + + print("Finding domain GUID from ncName") + res = ctx.local_samdb.search(base=ctx.partition_dn, scope=ldb.SCOPE_BASE, attrs=['ncName'], + controls=["extended_dn:1:1"]) + domguid = str(misc.GUID(ldb.Dn(ctx.samdb, res[0]['ncName'][0]).get_extended_component('GUID'))) + print("Got domain GUID %s" % domguid) + + print("Calling own domain provision") + + logger = logging.getLogger("provision") + logger.addHandler(logging.StreamHandler(sys.stdout)) + + secrets_ldb = Ldb(ctx.paths.secrets, session_info=system_session(), lp=ctx.lp) + + presult = provision_fill(ctx.local_samdb, secrets_ldb, + logger, ctx.names, ctx.paths, domainsid=security.dom_sid(ctx.domsid), + domainguid=domguid, + targetdir=ctx.targetdir, samdb_fill=FILL_SUBDOMAIN, + machinepass=ctx.acct_pass, serverrole="domain controller", + lp=ctx.lp, hostip=ctx.names.hostip, hostip6=ctx.names.hostip6, + dns_backend=ctx.dns_backend) + print("Provision OK for domain %s" % ctx.names.dnsdomain) + + def join_replicate(ctx): + """Replicate the SAM.""" + + print "Starting replication" + ctx.local_samdb.transaction_start() + try: + source_dsa_invocation_id = misc.GUID(ctx.samdb.get_invocation_id()) + if ctx.ntds_guid is None: + print("Using DS_BIND_GUID_W2K3") + destination_dsa_guid = misc.GUID(drsuapi.DRSUAPI_DS_BIND_GUID_W2K3) + else: + destination_dsa_guid = ctx.ntds_guid + + if ctx.RODC: + repl_creds = Credentials() + repl_creds.guess(ctx.lp) + repl_creds.set_kerberos_state(DONT_USE_KERBEROS) + repl_creds.set_username(ctx.samname) + repl_creds.set_password(ctx.acct_pass) + else: + repl_creds = ctx.creds + + binding_options = "seal" + if int(ctx.lp.get("log level")) >= 5: + binding_options += ",print" + repl = drs_utils.drs_Replicate( + "ncacn_ip_tcp:%s[%s]" % (ctx.server, binding_options), + ctx.lp, repl_creds, ctx.local_samdb) + + repl.replicate(ctx.schema_dn, source_dsa_invocation_id, + destination_dsa_guid, schema=True, rodc=ctx.RODC, + replica_flags=ctx.replica_flags) + repl.replicate(ctx.config_dn, source_dsa_invocation_id, + destination_dsa_guid, rodc=ctx.RODC, + replica_flags=ctx.replica_flags) + if not ctx.subdomain: + # Replicate first the critical object for the basedn + if not ctx.domain_replica_flags & drsuapi.DRSUAPI_DRS_CRITICAL_ONLY: + print "Replicating critical objects from the base DN of the domain" + ctx.domain_replica_flags |= drsuapi.DRSUAPI_DRS_CRITICAL_ONLY | drsuapi.DRSUAPI_DRS_GET_ANC + repl.replicate(ctx.base_dn, source_dsa_invocation_id, + destination_dsa_guid, rodc=ctx.RODC, + replica_flags=ctx.domain_replica_flags) + ctx.domain_replica_flags ^= drsuapi.DRSUAPI_DRS_CRITICAL_ONLY | drsuapi.DRSUAPI_DRS_GET_ANC + else: + ctx.domain_replica_flags |= drsuapi.DRSUAPI_DRS_GET_ANC + repl.replicate(ctx.base_dn, source_dsa_invocation_id, + destination_dsa_guid, rodc=ctx.RODC, + replica_flags=ctx.domain_replica_flags) + print "Done with always replicated NC (base, config, schema)" + + for nc in (ctx.domaindns_zone, ctx.forestdns_zone): + if nc in ctx.nc_list: + print "Replicating %s" % (str(nc)) + repl.replicate(nc, source_dsa_invocation_id, + destination_dsa_guid, rodc=ctx.RODC, + replica_flags=ctx.replica_flags) + + if 'DC=ForestDnsZones,%s' % ctx.root_dn in ctx.nc_list: + repl.replicate('DC=ForestDnsZones,%s' % ctx.root_dn, source_dsa_invocation_id, + destination_dsa_guid, rodc=ctx.RODC, + replica_flags=ctx.replica_flags) + # FIXME At this point we should add an entry in the forestdns and domaindns NC + # (those under CN=Partions,DC=...) + # in order to indicate that we hold a replica for this NC + + if ctx.RODC: + repl.replicate(ctx.acct_dn, source_dsa_invocation_id, + destination_dsa_guid, + exop=drsuapi.DRSUAPI_EXOP_REPL_SECRET, rodc=True) + repl.replicate(ctx.new_krbtgt_dn, source_dsa_invocation_id, + destination_dsa_guid, + exop=drsuapi.DRSUAPI_EXOP_REPL_SECRET, rodc=True) + ctx.repl = repl + ctx.source_dsa_invocation_id = source_dsa_invocation_id + ctx.destination_dsa_guid = destination_dsa_guid + + print "Committing SAM database" + except: + ctx.local_samdb.transaction_cancel() + raise + else: + ctx.local_samdb.transaction_commit() + + def send_DsReplicaUpdateRefs(ctx, dn): + r = drsuapi.DsReplicaUpdateRefsRequest1() + r.naming_context = drsuapi.DsReplicaObjectIdentifier() + r.naming_context.dn = str(dn) + r.naming_context.guid = misc.GUID("00000000-0000-0000-0000-000000000000") + r.naming_context.sid = security.dom_sid("S-0-0") + r.dest_dsa_guid = ctx.ntds_guid + r.dest_dsa_dns_name = "%s._msdcs.%s" % (str(ctx.ntds_guid), ctx.dnsforest) + r.options = drsuapi.DRSUAPI_DRS_ADD_REF | drsuapi.DRSUAPI_DRS_DEL_REF + if not ctx.RODC: + r.options |= drsuapi.DRSUAPI_DRS_WRIT_REP + + if ctx.drsuapi: + ctx.drsuapi.DsReplicaUpdateRefs(ctx.drsuapi_handle, 1, r) + + def join_finalise(ctx): + """Finalise the join, mark us synchronised and setup secrets db.""" + + logger = logging.getLogger("provision") + logger.addHandler(logging.StreamHandler(sys.stdout)) + + # FIXME we shouldn't do this in all cases + # If for some reasons we joined in another site than the one of + # DC we just replicated from then we don't need to send the updatereplicateref + # as replication between sites is time based and on the initiative of the + # requesting DC + print "Sending DsReplicateUpdateRefs for all the replicated partitions" + for nc in ctx.full_nc_list: + ctx.send_DsReplicaUpdateRefs(nc) + + if ctx.RODC: + print "Setting RODC invocationId" + ctx.local_samdb.set_invocation_id(str(ctx.invocation_id)) + ctx.local_samdb.set_opaque_integer("domainFunctionality", + ctx.behavior_version) + m = ldb.Message() + m.dn = ldb.Dn(ctx.local_samdb, "%s" % ctx.ntds_dn) + m["invocationId"] = ldb.MessageElement(ndr_pack(ctx.invocation_id), + ldb.FLAG_MOD_REPLACE, + "invocationId") + ctx.local_samdb.modify(m) + + # Note: as RODC the invocationId is only stored + # on the RODC itself, the other DCs never see it. + # + # Thats is why we fix up the replPropertyMetaData stamp + # for the 'invocationId' attribute, we need to change + # the 'version' to '0', this is what windows 2008r2 does as RODC + # + # This means if the object on a RWDC ever gets a invocationId + # attribute, it will have version '1' (or higher), which will + # will overwrite the RODC local value. + ctx.local_samdb.set_attribute_replmetadata_version(m.dn, + "invocationId", + 0) + + print "Setting isSynchronized and dsServiceName" + m = ldb.Message() + m.dn = ldb.Dn(ctx.local_samdb, '@ROOTDSE') + m["isSynchronized"] = ldb.MessageElement("TRUE", ldb.FLAG_MOD_REPLACE, "isSynchronized") + m["dsServiceName"] = ldb.MessageElement("<GUID=%s>" % str(ctx.ntds_guid), + ldb.FLAG_MOD_REPLACE, "dsServiceName") + ctx.local_samdb.modify(m) + + if ctx.subdomain: + return + + secrets_ldb = Ldb(ctx.paths.secrets, session_info=system_session(), lp=ctx.lp) + + print "Setting up secrets database" + secretsdb_self_join(secrets_ldb, domain=ctx.domain_name, + realm=ctx.realm, + dnsdomain=ctx.dnsdomain, + netbiosname=ctx.myname, + domainsid=security.dom_sid(ctx.domsid), + machinepass=ctx.acct_pass, + secure_channel_type=ctx.secure_channel_type, + key_version_number=ctx.key_version_number) + + if ctx.dns_backend.startswith("BIND9_"): + dnspass = samba.generate_random_password(128, 255) + + setup_bind9_dns(ctx.local_samdb, secrets_ldb, security.dom_sid(ctx.domsid), + ctx.names, ctx.paths, ctx.lp, logger, + dns_backend=ctx.dns_backend, + dnspass=dnspass, os_level=ctx.behavior_version, + targetdir=ctx.targetdir) + + def join_setup_trusts(ctx): + """provision the local SAM.""" + + def arcfour_encrypt(key, data): + from Crypto.Cipher import ARC4 + c = ARC4.new(key) + return c.encrypt(data) + + def string_to_array(string): + blob = [0] * len(string) + + for i in range(len(string)): + blob[i] = ord(string[i]) + + return blob + + print "Setup domain trusts with server %s" % ctx.server + binding_options = "" # why doesn't signing work here? w2k8r2 claims no session key + lsaconn = lsa.lsarpc("ncacn_np:%s[%s]" % (ctx.server, binding_options), + ctx.lp, ctx.creds) + + objectAttr = lsa.ObjectAttribute() + objectAttr.sec_qos = lsa.QosInfo() + + pol_handle = lsaconn.OpenPolicy2(''.decode('utf-8'), + objectAttr, security.SEC_FLAG_MAXIMUM_ALLOWED) + + info = lsa.TrustDomainInfoInfoEx() + info.domain_name.string = ctx.dnsdomain + info.netbios_name.string = ctx.domain_name + info.sid = security.dom_sid(ctx.domsid) + info.trust_direction = lsa.LSA_TRUST_DIRECTION_INBOUND | lsa.LSA_TRUST_DIRECTION_OUTBOUND + info.trust_type = lsa.LSA_TRUST_TYPE_UPLEVEL + info.trust_attributes = lsa.LSA_TRUST_ATTRIBUTE_WITHIN_FOREST + + try: + oldname = lsa.String() + oldname.string = ctx.dnsdomain + oldinfo = lsaconn.QueryTrustedDomainInfoByName(pol_handle, oldname, + lsa.LSA_TRUSTED_DOMAIN_INFO_FULL_INFO) + print("Removing old trust record for %s (SID %s)" % (ctx.dnsdomain, oldinfo.info_ex.sid)) + lsaconn.DeleteTrustedDomain(pol_handle, oldinfo.info_ex.sid) + except RuntimeError: + pass + + password_blob = string_to_array(ctx.trustdom_pass.encode('utf-16-le')) + + clear_value = drsblobs.AuthInfoClear() + clear_value.size = len(password_blob) + clear_value.password = password_blob + + clear_authentication_information = drsblobs.AuthenticationInformation() + clear_authentication_information.LastUpdateTime = samba.unix2nttime(int(time.time())) + clear_authentication_information.AuthType = lsa.TRUST_AUTH_TYPE_CLEAR + clear_authentication_information.AuthInfo = clear_value + + authentication_information_array = drsblobs.AuthenticationInformationArray() + authentication_information_array.count = 1 + authentication_information_array.array = [clear_authentication_information] + + outgoing = drsblobs.trustAuthInOutBlob() + outgoing.count = 1 + outgoing.current = authentication_information_array + + trustpass = drsblobs.trustDomainPasswords() + confounder = [3] * 512 + + for i in range(512): + confounder[i] = random.randint(0, 255) + + trustpass.confounder = confounder + + trustpass.outgoing = outgoing + trustpass.incoming = outgoing + + trustpass_blob = ndr_pack(trustpass) + + encrypted_trustpass = arcfour_encrypt(lsaconn.session_key, trustpass_blob) + + auth_blob = lsa.DATA_BUF2() + auth_blob.size = len(encrypted_trustpass) + auth_blob.data = string_to_array(encrypted_trustpass) + + auth_info = lsa.TrustDomainInfoAuthInfoInternal() + auth_info.auth_blob = auth_blob + + trustdom_handle = lsaconn.CreateTrustedDomainEx2(pol_handle, + info, + auth_info, + security.SEC_STD_DELETE) + + rec = { + "dn" : "cn=%s,cn=system,%s" % (ctx.dnsforest, ctx.base_dn), + "objectclass" : "trustedDomain", + "trustType" : str(info.trust_type), + "trustAttributes" : str(info.trust_attributes), + "trustDirection" : str(info.trust_direction), + "flatname" : ctx.forest_domain_name, + "trustPartner" : ctx.dnsforest, + "trustAuthIncoming" : ndr_pack(outgoing), + "trustAuthOutgoing" : ndr_pack(outgoing) + } + ctx.local_samdb.add(rec) + + rec = { + "dn" : "cn=%s$,cn=users,%s" % (ctx.forest_domain_name, ctx.base_dn), + "objectclass" : "user", + "userAccountControl" : str(samba.dsdb.UF_INTERDOMAIN_TRUST_ACCOUNT), + "clearTextPassword" : ctx.trustdom_pass.encode('utf-16-le') + } + ctx.local_samdb.add(rec) + + + def do_join(ctx): + # full_nc_list is the list of naming context (NC) for which we will + # send a updateRef command to the partner DC + ctx.nc_list = [ ctx.config_dn, ctx.schema_dn ] + ctx.full_nc_list = [ctx.base_dn, ctx.config_dn, ctx.schema_dn ] + + if not ctx.subdomain: + ctx.nc_list += [ctx.base_dn] + if ctx.dns_backend != "NONE": + ctx.nc_list += [ctx.domaindns_zone] + + if ctx.dns_backend != "NONE": + ctx.full_nc_list += ['DC=DomainDnsZones,%s' % ctx.base_dn] + ctx.full_nc_list += ['DC=ForestDnsZones,%s' % ctx.root_dn] + ctx.nc_list += ['DC=ForestDnsZones,%s' % ctx.root_dn] + + if ctx.promote_existing: + ctx.promote_possible() + else: + ctx.cleanup_old_join() + + try: + ctx.join_add_objects() + ctx.join_provision() + ctx.join_replicate() + if ctx.subdomain: + ctx.join_add_objects2() + ctx.join_provision_own_domain() + ctx.join_setup_trusts() + ctx.join_finalise() + except: + print "Join failed - cleaning up" + ctx.cleanup_old_join() + raise + + +def join_RODC(server=None, creds=None, lp=None, site=None, netbios_name=None, + targetdir=None, domain=None, domain_critical_only=False, + machinepass=None, use_ntvfs=False, dns_backend=None, + promote_existing=False): + """Join as a RODC.""" + + ctx = dc_join(server, creds, lp, site, netbios_name, targetdir, domain, + machinepass, use_ntvfs, dns_backend, promote_existing) + + lp.set("workgroup", ctx.domain_name) + print("workgroup is %s" % ctx.domain_name) + + lp.set("realm", ctx.realm) + print("realm is %s" % ctx.realm) + + ctx.krbtgt_dn = "CN=krbtgt_%s,CN=Users,%s" % (ctx.myname, ctx.base_dn) + + # setup some defaults for accounts that should be replicated to this RODC + ctx.never_reveal_sid = [ + "<SID=%s-%s>" % (ctx.domsid, security.DOMAIN_RID_RODC_DENY), + "<SID=%s>" % security.SID_BUILTIN_ADMINISTRATORS, + "<SID=%s>" % security.SID_BUILTIN_SERVER_OPERATORS, + "<SID=%s>" % security.SID_BUILTIN_BACKUP_OPERATORS, + "<SID=%s>" % security.SID_BUILTIN_ACCOUNT_OPERATORS] + ctx.reveal_sid = "<SID=%s-%s>" % (ctx.domsid, security.DOMAIN_RID_RODC_ALLOW) + + mysid = ctx.get_mysid() + admin_dn = "<SID=%s>" % mysid + ctx.managedby = admin_dn + + ctx.userAccountControl = (samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT | + samba.dsdb.UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION | + samba.dsdb.UF_PARTIAL_SECRETS_ACCOUNT) + + ctx.SPNs.extend([ "RestrictedKrbHost/%s" % ctx.myname, + "RestrictedKrbHost/%s" % ctx.dnshostname ]) + + ctx.connection_dn = "CN=RODC Connection (FRS),%s" % ctx.ntds_dn + ctx.secure_channel_type = misc.SEC_CHAN_RODC + ctx.RODC = True + ctx.replica_flags = (drsuapi.DRSUAPI_DRS_INIT_SYNC | + drsuapi.DRSUAPI_DRS_PER_SYNC | + drsuapi.DRSUAPI_DRS_GET_ANC | + drsuapi.DRSUAPI_DRS_NEVER_SYNCED | + drsuapi.DRSUAPI_DRS_SPECIAL_SECRET_PROCESSING | + drsuapi.DRSUAPI_DRS_GET_ALL_GROUP_MEMBERSHIP) + ctx.domain_replica_flags = ctx.replica_flags + if domain_critical_only: + ctx.domain_replica_flags |= drsuapi.DRSUAPI_DRS_CRITICAL_ONLY + + ctx.do_join() + + print "Joined domain %s (SID %s) as an RODC" % (ctx.domain_name, ctx.domsid) + + +def join_DC(server=None, creds=None, lp=None, site=None, netbios_name=None, + targetdir=None, domain=None, domain_critical_only=False, + machinepass=None, use_ntvfs=False, dns_backend=None, + promote_existing=False): + """Join as a DC.""" + ctx = dc_join(server, creds, lp, site, netbios_name, targetdir, domain, + machinepass, use_ntvfs, dns_backend, promote_existing) + + lp.set("workgroup", ctx.domain_name) + print("workgroup is %s" % ctx.domain_name) + + lp.set("realm", ctx.realm) + print("realm is %s" % ctx.realm) + + ctx.userAccountControl = samba.dsdb.UF_SERVER_TRUST_ACCOUNT | samba.dsdb.UF_TRUSTED_FOR_DELEGATION + + ctx.SPNs.append('E3514235-4B06-11D1-AB04-00C04FC2DCD2/$NTDSGUID/%s' % ctx.dnsdomain) + ctx.secure_channel_type = misc.SEC_CHAN_BDC + + ctx.replica_flags = (drsuapi.DRSUAPI_DRS_WRIT_REP | + drsuapi.DRSUAPI_DRS_INIT_SYNC | + drsuapi.DRSUAPI_DRS_PER_SYNC | + drsuapi.DRSUAPI_DRS_FULL_SYNC_IN_PROGRESS | + drsuapi.DRSUAPI_DRS_NEVER_SYNCED) + ctx.domain_replica_flags = ctx.replica_flags + if domain_critical_only: + ctx.domain_replica_flags |= drsuapi.DRSUAPI_DRS_CRITICAL_ONLY + + ctx.do_join() + print "Joined domain %s (SID %s) as a DC" % (ctx.domain_name, ctx.domsid) + +def join_subdomain(server=None, creds=None, lp=None, site=None, + netbios_name=None, targetdir=None, parent_domain=None, dnsdomain=None, + netbios_domain=None, machinepass=None, use_ntvfs=False, + dns_backend=None): + """Join as a DC.""" + ctx = dc_join(server, creds, lp, site, netbios_name, targetdir, parent_domain, + machinepass, use_ntvfs, dns_backend) + ctx.subdomain = True + ctx.parent_domain_name = ctx.domain_name + ctx.domain_name = netbios_domain + ctx.realm = dnsdomain + ctx.parent_dnsdomain = ctx.dnsdomain + ctx.parent_partition_dn = ctx.get_parent_partition_dn() + ctx.dnsdomain = dnsdomain + ctx.partition_dn = "CN=%s,CN=Partitions,%s" % (ctx.domain_name, ctx.config_dn) + ctx.naming_master = ctx.get_naming_master() + if ctx.naming_master != ctx.server: + print("Reconnecting to naming master %s" % ctx.naming_master) + ctx.server = ctx.naming_master + ctx.samdb = SamDB(url="ldap://%s" % ctx.server, + session_info=system_session(), + credentials=ctx.creds, lp=ctx.lp) + + ctx.base_dn = samba.dn_from_dns_name(dnsdomain) + ctx.domsid = str(security.random_sid()) + ctx.acct_dn = None + ctx.dnshostname = "%s.%s" % (ctx.myname, ctx.dnsdomain) + ctx.trustdom_pass = samba.generate_random_password(128, 128) + + ctx.userAccountControl = samba.dsdb.UF_SERVER_TRUST_ACCOUNT | samba.dsdb.UF_TRUSTED_FOR_DELEGATION + + ctx.SPNs.append('E3514235-4B06-11D1-AB04-00C04FC2DCD2/$NTDSGUID/%s' % ctx.dnsdomain) + ctx.secure_channel_type = misc.SEC_CHAN_BDC + + ctx.replica_flags = (drsuapi.DRSUAPI_DRS_WRIT_REP | + drsuapi.DRSUAPI_DRS_INIT_SYNC | + drsuapi.DRSUAPI_DRS_PER_SYNC | + drsuapi.DRSUAPI_DRS_FULL_SYNC_IN_PROGRESS | + drsuapi.DRSUAPI_DRS_NEVER_SYNCED) + ctx.domain_replica_flags = ctx.replica_flags + + ctx.do_join() + print "Created domain %s (SID %s) as a DC" % (ctx.domain_name, ctx.domsid) diff --git a/python/samba/kcc_utils.py b/python/samba/kcc_utils.py new file mode 100644 index 00000000000..57c31876a69 --- /dev/null +++ b/python/samba/kcc_utils.py @@ -0,0 +1,2182 @@ +# KCC topology utilities +# +# Copyright (C) Dave Craft 2011 +# Copyright (C) Jelmer Vernooij 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 +import uuid +import time + +from samba import dsdb, unix2nttime +from samba.dcerpc import ( + drsblobs, + drsuapi, + misc, + ) +from samba.common import dsdb_Dn +from samba.ndr import (ndr_unpack, ndr_pack) + + +class NCType(object): + (unknown, schema, domain, config, application) = range(0, 5) + + +class NamingContext(object): + """Base class for a naming context. + + Holds the DN, GUID, SID (if available) and type of the DN. + Subclasses may inherit from this and specialize + """ + + def __init__(self, nc_dnstr): + """Instantiate a NamingContext + + :param nc_dnstr: NC dn string + """ + self.nc_dnstr = nc_dnstr + self.nc_guid = None + self.nc_sid = None + self.nc_type = NCType.unknown + + def __str__(self): + '''Debug dump string output of class''' + text = "%s:" % self.__class__.__name__ + text = text + "\n\tnc_dnstr=%s" % self.nc_dnstr + text = text + "\n\tnc_guid=%s" % str(self.nc_guid) + + if self.nc_sid is None: + text = text + "\n\tnc_sid=<absent>" + else: + text = text + "\n\tnc_sid=<present>" + + text = text + "\n\tnc_type=%s" % self.nc_type + return text + + def load_nc(self, samdb): + attrs = [ "objectGUID", + "objectSid" ] + try: + res = samdb.search(base=self.nc_dnstr, + scope=ldb.SCOPE_BASE, attrs=attrs) + + except ldb.LdbError, (enum, estr): + raise Exception("Unable to find naming context (%s)" % + (self.nc_dnstr, estr)) + msg = res[0] + if "objectGUID" in msg: + self.nc_guid = misc.GUID(samdb.schema_format_value("objectGUID", + msg["objectGUID"][0])) + if "objectSid" in msg: + self.nc_sid = msg["objectSid"][0] + + assert self.nc_guid is not None + + def is_schema(self): + '''Return True if NC is schema''' + assert self.nc_type != NCType.unknown + return self.nc_type == NCType.schema + + def is_domain(self): + '''Return True if NC is domain''' + assert self.nc_type != NCType.unknown + return self.nc_type == NCType.domain + + def is_application(self): + '''Return True if NC is application''' + assert self.nc_type != NCType.unknown + return self.nc_type == NCType.application + + def is_config(self): + '''Return True if NC is config''' + assert self.nc_type != NCType.unknown + return self.nc_type == NCType.config + + def identify_by_basedn(self, samdb): + """Given an NC object, identify what type is is thru + the samdb basedn strings and NC sid value + """ + # Invoke loader to initialize guid and more + # importantly sid value (sid is used to identify + # domain NCs) + if self.nc_guid is None: + self.load_nc(samdb) + + # We check against schema and config because they + # will be the same for all nTDSDSAs in the forest. + # That leaves the domain NCs which can be identified + # by sid and application NCs as the last identified + if self.nc_dnstr == str(samdb.get_schema_basedn()): + self.nc_type = NCType.schema + elif self.nc_dnstr == str(samdb.get_config_basedn()): + self.nc_type = NCType.config + elif self.nc_sid is not None: + self.nc_type = NCType.domain + else: + self.nc_type = NCType.application + + def identify_by_dsa_attr(self, samdb, attr): + """Given an NC which has been discovered thru the + nTDSDSA database object, determine what type of NC + it is (i.e. schema, config, domain, application) via + the use of the schema attribute under which the NC + was found. + + :param attr: attr of nTDSDSA object where NC DN appears + """ + # If the NC is listed under msDS-HasDomainNCs then + # this can only be a domain NC and it is our default + # domain for this dsa + if attr == "msDS-HasDomainNCs": + self.nc_type = NCType.domain + + # If the NC is listed under hasPartialReplicaNCs + # this is only a domain NC + elif attr == "hasPartialReplicaNCs": + self.nc_type = NCType.domain + + # NCs listed under hasMasterNCs are either + # default domain, schema, or config. We + # utilize the identify_by_basedn() to + # identify those + elif attr == "hasMasterNCs": + self.identify_by_basedn(samdb) + + # Still unknown (unlikely) but for completeness + # and for finally identifying application NCs + if self.nc_type == NCType.unknown: + self.identify_by_basedn(samdb) + + +class NCReplica(NamingContext): + """Naming context replica that is relative to a specific DSA. + + This is a more specific form of NamingContext class (inheriting from that + class) and it identifies unique attributes of the DSA's replica for a NC. + """ + + def __init__(self, dsa_dnstr, dsa_guid, nc_dnstr): + """Instantiate a Naming Context Replica + + :param dsa_guid: GUID of DSA where replica appears + :param nc_dnstr: NC dn string + """ + self.rep_dsa_dnstr = dsa_dnstr + self.rep_dsa_guid = dsa_guid + self.rep_default = False # replica for DSA's default domain + self.rep_partial = False + self.rep_ro = False + self.rep_instantiated_flags = 0 + + self.rep_fsmo_role_owner = None + + # RepsFromTo tuples + self.rep_repsFrom = [] + + # The (is present) test is a combination of being + # enumerated in (hasMasterNCs or msDS-hasFullReplicaNCs or + # hasPartialReplicaNCs) as well as its replica flags found + # thru the msDS-HasInstantiatedNCs. If the NC replica meets + # the first enumeration test then this flag is set true + self.rep_present_criteria_one = False + + # Call my super class we inherited from + NamingContext.__init__(self, nc_dnstr) + + def __str__(self): + '''Debug dump string output of class''' + text = "%s:" % self.__class__.__name__ + text = text + "\n\tdsa_dnstr=%s" % self.rep_dsa_dnstr + text = text + "\n\tdsa_guid=%s" % str(self.rep_dsa_guid) + text = text + "\n\tdefault=%s" % self.rep_default + text = text + "\n\tro=%s" % self.rep_ro + text = text + "\n\tpartial=%s" % self.rep_partial + text = text + "\n\tpresent=%s" % self.is_present() + text = text + "\n\tfsmo_role_owner=%s" % self.rep_fsmo_role_owner + + for rep in self.rep_repsFrom: + text = text + "\n%s" % rep + + return "%s\n%s" % (NamingContext.__str__(self), text) + + def set_instantiated_flags(self, flags=None): + '''Set or clear NC replica instantiated flags''' + if flags is None: + self.rep_instantiated_flags = 0 + else: + self.rep_instantiated_flags = flags + + def identify_by_dsa_attr(self, samdb, attr): + """Given an NC which has been discovered thru the + nTDSDSA database object, determine what type of NC + replica it is (i.e. partial, read only, default) + + :param attr: attr of nTDSDSA object where NC DN appears + """ + # If the NC was found under hasPartialReplicaNCs + # then a partial replica at this dsa + if attr == "hasPartialReplicaNCs": + self.rep_partial = True + self.rep_present_criteria_one = True + + # If the NC is listed under msDS-HasDomainNCs then + # this can only be a domain NC and it is the DSA's + # default domain NC + elif attr == "msDS-HasDomainNCs": + self.rep_default = True + + # NCs listed under hasMasterNCs are either + # default domain, schema, or config. We check + # against schema and config because they will be + # the same for all nTDSDSAs in the forest. That + # leaves the default domain NC remaining which + # may be different for each nTDSDSAs (and thus + # we don't compare agains this samdb's default + # basedn + elif attr == "hasMasterNCs": + self.rep_present_criteria_one = True + + if self.nc_dnstr != str(samdb.get_schema_basedn()) and \ + self.nc_dnstr != str(samdb.get_config_basedn()): + self.rep_default = True + + # RODC only + elif attr == "msDS-hasFullReplicaNCs": + self.rep_present_criteria_one = True + self.rep_ro = True + + # Not RODC + elif attr == "msDS-hasMasterNCs": + self.rep_ro = False + + # Now use this DSA attribute to identify the naming + # context type by calling the super class method + # of the same name + NamingContext.identify_by_dsa_attr(self, samdb, attr) + + def is_default(self): + """Whether this is a default domain for the dsa that this NC appears on + """ + return self.rep_default + + def is_ro(self): + '''Return True if NC replica is read only''' + return self.rep_ro + + def is_partial(self): + '''Return True if NC replica is partial''' + return self.rep_partial + + def is_present(self): + """Given an NC replica which has been discovered thru the + nTDSDSA database object and populated with replica flags + from the msDS-HasInstantiatedNCs; return whether the NC + replica is present (true) or if the IT_NC_GOING flag is + set then the NC replica is not present (false) + """ + if self.rep_present_criteria_one and \ + self.rep_instantiated_flags & dsdb.INSTANCE_TYPE_NC_GOING == 0: + return True + return False + + def load_repsFrom(self, samdb): + """Given an NC replica which has been discovered thru the nTDSDSA + database object, load the repsFrom attribute for the local replica. + held by my dsa. The repsFrom attribute is not replicated so this + attribute is relative only to the local DSA that the samdb exists on + """ + try: + res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE, + attrs=[ "repsFrom" ]) + + except ldb.LdbError, (enum, estr): + raise Exception("Unable to find NC for (%s) - (%s)" % + (self.nc_dnstr, estr)) + + msg = res[0] + + # Possibly no repsFrom if this is a singleton DC + if "repsFrom" in msg: + for value in msg["repsFrom"]: + rep = RepsFromTo(self.nc_dnstr, + ndr_unpack(drsblobs.repsFromToBlob, value)) + self.rep_repsFrom.append(rep) + + def commit_repsFrom(self, samdb, ro=False): + """Commit repsFrom to the database""" + + # XXX - This is not truly correct according to the MS-TECH + # docs. To commit a repsFrom we should be using RPCs + # IDL_DRSReplicaAdd, IDL_DRSReplicaModify, and + # IDL_DRSReplicaDel to affect a repsFrom change. + # + # Those RPCs are missing in samba, so I'll have to + # implement them to get this to more accurately + # reflect the reference docs. As of right now this + # commit to the database will work as its what the + # older KCC also did + modify = False + newreps = [] + delreps = [] + + for repsFrom in self.rep_repsFrom: + + # Leave out any to be deleted from + # replacement list. Build a list + # of to be deleted reps which we will + # remove from rep_repsFrom list below + if repsFrom.to_be_deleted: + delreps.append(repsFrom) + modify = True + continue + + if repsFrom.is_modified(): + repsFrom.set_unmodified() + modify = True + + # current (unmodified) elements also get + # appended here but no changes will occur + # unless something is "to be modified" or + # "to be deleted" + newreps.append(ndr_pack(repsFrom.ndr_blob)) + + # Now delete these from our list of rep_repsFrom + for repsFrom in delreps: + self.rep_repsFrom.remove(repsFrom) + delreps = [] + + # Nothing to do if no reps have been modified or + # need to be deleted or input option has informed + # us to be "readonly" (ro). Leave database + # record "as is" + if not modify or ro: + return + + m = ldb.Message() + m.dn = ldb.Dn(samdb, self.nc_dnstr) + + m["repsFrom"] = \ + ldb.MessageElement(newreps, ldb.FLAG_MOD_REPLACE, "repsFrom") + + try: + samdb.modify(m) + + except ldb.LdbError, estr: + raise Exception("Could not set repsFrom for (%s) - (%s)" % + (self.dsa_dnstr, estr)) + + def dumpstr_to_be_deleted(self): + text="" + for repsFrom in self.rep_repsFrom: + if repsFrom.to_be_deleted: + if text: + text = text + "\n%s" % repsFrom + else: + text = "%s" % repsFrom + return text + + def dumpstr_to_be_modified(self): + text="" + for repsFrom in self.rep_repsFrom: + if repsFrom.is_modified(): + if text: + text = text + "\n%s" % repsFrom + else: + text = "%s" % repsFrom + return text + + def load_fsmo_roles(self, samdb): + """Given an NC replica which has been discovered thru the nTDSDSA + database object, load the fSMORoleOwner attribute. + """ + try: + res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE, + attrs=[ "fSMORoleOwner" ]) + + except ldb.LdbError, (enum, estr): + raise Exception("Unable to find NC for (%s) - (%s)" % + (self.nc_dnstr, estr)) + + msg = res[0] + + # Possibly no fSMORoleOwner + if "fSMORoleOwner" in msg: + self.rep_fsmo_role_owner = msg["fSMORoleOwner"] + + def is_fsmo_role_owner(self, dsa_dnstr): + if self.rep_fsmo_role_owner is not None and \ + self.rep_fsmo_role_owner == dsa_dnstr: + return True + return False + + +class DirectoryServiceAgent(object): + + def __init__(self, dsa_dnstr): + """Initialize DSA class. + + Class is subsequently fully populated by calling the load_dsa() method + + :param dsa_dnstr: DN of the nTDSDSA + """ + self.dsa_dnstr = dsa_dnstr + self.dsa_guid = None + self.dsa_ivid = None + self.dsa_is_ro = False + self.dsa_is_istg = False + self.dsa_options = 0 + self.dsa_behavior = 0 + self.default_dnstr = None # default domain dn string for dsa + + # NCReplicas for this dsa that are "present" + # Indexed by DN string of naming context + self.current_rep_table = {} + + # NCReplicas for this dsa that "should be present" + # Indexed by DN string of naming context + self.needed_rep_table = {} + + # NTDSConnections for this dsa. These are current + # valid connections that are committed or pending a commit + # in the database. Indexed by DN string of connection + self.connect_table = {} + + def __str__(self): + '''Debug dump string output of class''' + + text = "%s:" % self.__class__.__name__ + if self.dsa_dnstr is not None: + text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr + if self.dsa_guid is not None: + text = text + "\n\tdsa_guid=%s" % str(self.dsa_guid) + if self.dsa_ivid is not None: + text = text + "\n\tdsa_ivid=%s" % str(self.dsa_ivid) + + text = text + "\n\tro=%s" % self.is_ro() + text = text + "\n\tgc=%s" % self.is_gc() + text = text + "\n\tistg=%s" % self.is_istg() + + text = text + "\ncurrent_replica_table:" + text = text + "\n%s" % self.dumpstr_current_replica_table() + text = text + "\nneeded_replica_table:" + text = text + "\n%s" % self.dumpstr_needed_replica_table() + text = text + "\nconnect_table:" + text = text + "\n%s" % self.dumpstr_connect_table() + + return text + + def get_current_replica(self, nc_dnstr): + if nc_dnstr in self.current_rep_table.keys(): + return self.current_rep_table[nc_dnstr] + else: + return None + + def is_istg(self): + '''Returns True if dsa is intersite topology generator for it's site''' + # The KCC on an RODC always acts as an ISTG for itself + return self.dsa_is_istg or self.dsa_is_ro + + def is_ro(self): + '''Returns True if dsa a read only domain controller''' + return self.dsa_is_ro + + def is_gc(self): + '''Returns True if dsa hosts a global catalog''' + if (self.options & dsdb.DS_NTDSDSA_OPT_IS_GC) != 0: + return True + return False + + def is_minimum_behavior(self, version): + """Is dsa at minimum windows level greater than or equal to (version) + + :param version: Windows version to test against + (e.g. DS_BEHAVIOR_WIN2008) + """ + if self.dsa_behavior >= version: + return True + return False + + def is_translate_ntdsconn_disabled(self): + """Whether this allows NTDSConnection translation in its options.""" + if (self.options & dsdb.DS_NTDSDSA_OPT_DISABLE_NTDSCONN_XLATE) != 0: + return True + return False + + def get_rep_tables(self): + """Return DSA current and needed replica tables + """ + return self.current_rep_table, self.needed_rep_table + + def get_parent_dnstr(self): + """Get the parent DN string of this object.""" + head, sep, tail = self.dsa_dnstr.partition(',') + return tail + + def load_dsa(self, samdb): + """Load a DSA from the samdb. + + Prior initialization has given us the DN of the DSA that we are to + load. This method initializes all other attributes, including loading + the NC replica table for this DSA. + """ + attrs = ["objectGUID", + "invocationID", + "options", + "msDS-isRODC", + "msDS-Behavior-Version"] + try: + res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE, + attrs=attrs) + + except ldb.LdbError, (enum, estr): + raise Exception("Unable to find nTDSDSA for (%s) - (%s)" % + (self.dsa_dnstr, estr)) + + msg = res[0] + self.dsa_guid = misc.GUID(samdb.schema_format_value("objectGUID", + msg["objectGUID"][0])) + + # RODCs don't originate changes and thus have no invocationId, + # therefore we must check for existence first + if "invocationId" in msg: + self.dsa_ivid = misc.GUID(samdb.schema_format_value("objectGUID", + msg["invocationId"][0])) + + if "options" in msg: + self.options = int(msg["options"][0]) + + if "msDS-isRODC" in msg and msg["msDS-isRODC"][0] == "TRUE": + self.dsa_is_ro = True + else: + self.dsa_is_ro = False + + if "msDS-Behavior-Version" in msg: + self.dsa_behavior = int(msg['msDS-Behavior-Version'][0]) + + # Load the NC replicas that are enumerated on this dsa + self.load_current_replica_table(samdb) + + # Load the nTDSConnection that are enumerated on this dsa + self.load_connection_table(samdb) + + def load_current_replica_table(self, samdb): + """Method to load the NC replica's listed for DSA object. + + This method queries the samdb for (hasMasterNCs, msDS-hasMasterNCs, + hasPartialReplicaNCs, msDS-HasDomainNCs, msDS-hasFullReplicaNCs, and + msDS-HasInstantiatedNCs) to determine complete list of NC replicas that + are enumerated for the DSA. Once a NC replica is loaded it is + identified (schema, config, etc) and the other replica attributes + (partial, ro, etc) are determined. + + :param samdb: database to query for DSA replica list + """ + ncattrs = [ # not RODC - default, config, schema (old style) + "hasMasterNCs", + # not RODC - default, config, schema, app NCs + "msDS-hasMasterNCs", + # domain NC partial replicas + "hasPartialReplicaNCs", + # default domain NC + "msDS-HasDomainNCs", + # RODC only - default, config, schema, app NCs + "msDS-hasFullReplicaNCs", + # Identifies if replica is coming, going, or stable + "msDS-HasInstantiatedNCs" ] + try: + res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE, + attrs=ncattrs) + + except ldb.LdbError, (enum, estr): + raise Exception("Unable to find nTDSDSA NCs for (%s) - (%s)" % + (self.dsa_dnstr, estr)) + + # The table of NCs for the dsa we are searching + tmp_table = {} + + # We should get one response to our query here for + # the ntds that we requested + if len(res[0]) > 0: + + # Our response will contain a number of elements including + # the dn of the dsa as well as elements for each + # attribute (e.g. hasMasterNCs). Each of these elements + # is a dictonary list which we retrieve the keys for and + # then iterate over them + for k in res[0].keys(): + if k == "dn": + continue + + # For each attribute type there will be one or more DNs + # listed. For instance DCs normally have 3 hasMasterNCs + # listed. + for value in res[0][k]: + # Turn dn into a dsdb_Dn so we can use + # its methods to parse a binary DN + dsdn = dsdb_Dn(samdb, value) + flags = dsdn.get_binary_integer() + dnstr = str(dsdn.dn) + + if not dnstr in tmp_table.keys(): + rep = NCReplica(self.dsa_dnstr, self.dsa_guid, dnstr) + tmp_table[dnstr] = rep + else: + rep = tmp_table[dnstr] + + if k == "msDS-HasInstantiatedNCs": + rep.set_instantiated_flags(flags) + continue + + rep.identify_by_dsa_attr(samdb, k) + + # if we've identified the default domain NC + # then save its DN string + if rep.is_default(): + self.default_dnstr = dnstr + else: + raise Exception("No nTDSDSA NCs for (%s)" % self.dsa_dnstr) + + # Assign our newly built NC replica table to this dsa + self.current_rep_table = tmp_table + + def add_needed_replica(self, rep): + """Method to add a NC replica that "should be present" to the + needed_rep_table if not already in the table + """ + if not rep.nc_dnstr in self.needed_rep_table.keys(): + self.needed_rep_table[rep.nc_dnstr] = rep + + def load_connection_table(self, samdb): + """Method to load the nTDSConnections listed for DSA object. + + :param samdb: database to query for DSA connection list + """ + try: + res = samdb.search(base=self.dsa_dnstr, + scope=ldb.SCOPE_SUBTREE, + expression="(objectClass=nTDSConnection)") + + except ldb.LdbError, (enum, estr): + raise Exception("Unable to find nTDSConnection for (%s) - (%s)" % + (self.dsa_dnstr, estr)) + + for msg in res: + dnstr = str(msg.dn) + + # already loaded + if dnstr in self.connect_table.keys(): + continue + + connect = NTDSConnection(dnstr) + + connect.load_connection(samdb) + self.connect_table[dnstr] = connect + + def commit_connections(self, samdb, ro=False): + """Method to commit any uncommitted nTDSConnections + modifications that are in our table. These would be + identified connections that are marked to be added or + deleted + + :param samdb: database to commit DSA connection list to + :param ro: if (true) then peform internal operations but + do not write to the database (readonly) + """ + delconn = [] + + for dnstr, connect in self.connect_table.items(): + if connect.to_be_added: + connect.commit_added(samdb, ro) + + if connect.to_be_modified: + connect.commit_modified(samdb, ro) + + if connect.to_be_deleted: + connect.commit_deleted(samdb, ro) + delconn.append(dnstr) + + # Now delete the connection from the table + for dnstr in delconn: + del self.connect_table[dnstr] + + def add_connection(self, dnstr, connect): + assert dnstr not in self.connect_table.keys() + self.connect_table[dnstr] = connect + + def get_connection_by_from_dnstr(self, from_dnstr): + """Scan DSA nTDSConnection table and return connection + with a "fromServer" dn string equivalent to method + input parameter. + + :param from_dnstr: search for this from server entry + """ + for dnstr, connect in self.connect_table.items(): + if connect.get_from_dnstr() == from_dnstr: + return connect + return None + + def dumpstr_current_replica_table(self): + '''Debug dump string output of current replica table''' + text="" + for k in self.current_rep_table.keys(): + if text: + text = text + "\n%s" % self.current_rep_table[k] + else: + text = "%s" % self.current_rep_table[k] + return text + + def dumpstr_needed_replica_table(self): + '''Debug dump string output of needed replica table''' + text="" + for k in self.needed_rep_table.keys(): + if text: + text = text + "\n%s" % self.needed_rep_table[k] + else: + text = "%s" % self.needed_rep_table[k] + return text + + def dumpstr_connect_table(self): + '''Debug dump string output of connect table''' + text="" + for k in self.connect_table.keys(): + if text: + text = text + "\n%s" % self.connect_table[k] + else: + text = "%s" % self.connect_table[k] + return text + + def new_connection(self, options, flags, transport, from_dnstr, sched): + """Set up a new connection for the DSA based on input + parameters. Connection will be added to the DSA + connect_table and will be marked as "to be added" pending + a call to commit_connections() + """ + dnstr = "CN=%s," % str(uuid.uuid4()) + self.dsa_dnstr + + connect = NTDSConnection(dnstr) + connect.to_be_added = True + connect.enabled = True + connect.from_dnstr = from_dnstr + connect.options = options + connect.flags = flags + + if transport is not None: + connect.transport_dnstr = transport.dnstr + + if sched is not None: + connect.schedule = sched + else: + # Create schedule. Attribute valuse set according to MS-TECH + # intrasite connection creation document + connect.schedule = drsblobs.schedule() + + connect.schedule.size = 188 + connect.schedule.bandwidth = 0 + connect.schedule.numberOfSchedules = 1 + + header = drsblobs.scheduleHeader() + header.type = 0 + header.offset = 20 + + connect.schedule.headerArray = [ header ] + + # 168 byte instances of the 0x01 value. The low order 4 bits + # of the byte equate to 15 minute intervals within a single hour. + # There are 168 bytes because there are 168 hours in a full week + # Effectively we are saying to perform replication at the end of + # each hour of the week + data = drsblobs.scheduleSlots() + data.slots = [ 0x01 ] * 168 + + connect.schedule.dataArray = [ data ] + + self.add_connection(dnstr, connect); + return connect + + +class NTDSConnection(object): + """Class defines a nTDSConnection found under a DSA + """ + def __init__(self, dnstr): + self.dnstr = dnstr + self.guid = None + self.enabled = False + self.whenCreated = 0 + self.to_be_added = False # new connection needs to be added + self.to_be_deleted = False # old connection needs to be deleted + self.to_be_modified = False + self.options = 0 + self.system_flags = 0 + self.transport_dnstr = None + self.transport_guid = None + self.from_dnstr = None + self.schedule = None + + def __str__(self): + '''Debug dump string output of NTDSConnection object''' + + text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr) + text = text + "\n\tenabled=%s" % self.enabled + text = text + "\n\tto_be_added=%s" % self.to_be_added + text = text + "\n\tto_be_deleted=%s" % self.to_be_deleted + text = text + "\n\tto_be_modified=%s" % self.to_be_modified + text = text + "\n\toptions=0x%08X" % self.options + text = text + "\n\tsystem_flags=0x%08X" % self.system_flags + text = text + "\n\twhenCreated=%d" % self.whenCreated + text = text + "\n\ttransport_dn=%s" % self.transport_dnstr + + if self.guid is not None: + text = text + "\n\tguid=%s" % str(self.guid) + + if self.transport_guid is not None: + text = text + "\n\ttransport_guid=%s" % str(self.transport_guid) + + text = text + "\n\tfrom_dn=%s" % self.from_dnstr + + if self.schedule is not None: + text = text + "\n\tschedule.size=%s" % self.schedule.size + text = text + "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth + text = text + "\n\tschedule.numberOfSchedules=%s" % \ + self.schedule.numberOfSchedules + + for i, header in enumerate(self.schedule.headerArray): + text = text + "\n\tschedule.headerArray[%d].type=%d" % \ + (i, header.type) + text = text + "\n\tschedule.headerArray[%d].offset=%d" % \ + (i, header.offset) + text = text + "\n\tschedule.dataArray[%d].slots[ " % i + for slot in self.schedule.dataArray[i].slots: + text = text + "0x%X " % slot + text = text + "]" + + return text + + def load_connection(self, samdb): + """Given a NTDSConnection object with an prior initialization + for the object's DN, search for the DN and load attributes + from the samdb. + """ + attrs = [ "options", + "enabledConnection", + "schedule", + "whenCreated", + "objectGUID", + "transportType", + "fromServer", + "systemFlags" ] + try: + res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE, + attrs=attrs) + + except ldb.LdbError, (enum, estr): + raise Exception("Unable to find nTDSConnection for (%s) - (%s)" % + (self.dnstr, estr)) + + msg = res[0] + + if "options" in msg: + self.options = int(msg["options"][0]) + + if "enabledConnection" in msg: + if msg["enabledConnection"][0].upper().lstrip().rstrip() == "TRUE": + self.enabled = True + + if "systemFlags" in msg: + self.system_flags = int(msg["systemFlags"][0]) + + if "objectGUID" in msg: + self.guid = \ + misc.GUID(samdb.schema_format_value("objectGUID", + msg["objectGUID"][0])) + + if "transportType" in msg: + dsdn = dsdb_Dn(samdb, msg["tranportType"][0]) + self.load_connection_transport(str(dsdn.dn)) + + if "schedule" in msg: + self.schedule = ndr_unpack(drsblobs.replSchedule, msg["schedule"][0]) + + if "whenCreated" in msg: + self.whenCreated = ldb.string_to_time(msg["whenCreated"][0]) + + if "fromServer" in msg: + dsdn = dsdb_Dn(samdb, msg["fromServer"][0]) + self.from_dnstr = str(dsdn.dn) + assert self.from_dnstr is not None + + def load_connection_transport(self, tdnstr): + """Given a NTDSConnection object which enumerates a transport + DN, load the transport information for the connection object + + :param tdnstr: transport DN to load + """ + attrs = [ "objectGUID" ] + try: + res = samdb.search(base=tdnstr, + scope=ldb.SCOPE_BASE, attrs=attrs) + + except ldb.LdbError, (enum, estr): + raise Exception("Unable to find transport (%s)" % + (tdnstr, estr)) + + if "objectGUID" in res[0]: + self.transport_dnstr = tdnstr + self.transport_guid = \ + misc.GUID(samdb.schema_format_value("objectGUID", + msg["objectGUID"][0])) + assert self.transport_dnstr is not None + assert self.transport_guid is not None + + def commit_deleted(self, samdb, ro=False): + """Local helper routine for commit_connections() which + handles committed connections that are to be deleted from + the database database + """ + assert self.to_be_deleted + self.to_be_deleted = False + + # No database modification requested + if ro: + return + + try: + samdb.delete(self.dnstr) + except ldb.LdbError, (enum, estr): + raise Exception("Could not delete nTDSConnection for (%s) - (%s)" % + (self.dnstr, estr)) + + def commit_added(self, samdb, ro=False): + """Local helper routine for commit_connections() which + handles committed connections that are to be added to the + database + """ + assert self.to_be_added + self.to_be_added = False + + # No database modification requested + if ro: + return + + # First verify we don't have this entry to ensure nothing + # is programatically amiss + found = False + try: + msg = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE) + if len(msg) != 0: + found = True + + except ldb.LdbError, (enum, estr): + if enum != ldb.ERR_NO_SUCH_OBJECT: + raise Exception("Unable to search for (%s) - (%s)" % + (self.dnstr, estr)) + if found: + raise Exception("nTDSConnection for (%s) already exists!" % + self.dnstr) + + if self.enabled: + enablestr = "TRUE" + else: + enablestr = "FALSE" + + # Prepare a message for adding to the samdb + m = ldb.Message() + m.dn = ldb.Dn(samdb, self.dnstr) + + m["objectClass"] = \ + ldb.MessageElement("nTDSConnection", ldb.FLAG_MOD_ADD, + "objectClass") + m["showInAdvancedViewOnly"] = \ + ldb.MessageElement("TRUE", ldb.FLAG_MOD_ADD, + "showInAdvancedViewOnly") + m["enabledConnection"] = \ + ldb.MessageElement(enablestr, ldb.FLAG_MOD_ADD, "enabledConnection") + m["fromServer"] = \ + ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_ADD, "fromServer") + m["options"] = \ + ldb.MessageElement(str(self.options), ldb.FLAG_MOD_ADD, "options") + m["systemFlags"] = \ + ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_ADD, + "systemFlags") + + if self.transport_dnstr is not None: + m["transportType"] = \ + ldb.MessageElement(str(self.transport_dnstr), ldb.FLAG_MOD_ADD, + "transportType") + + if self.schedule is not None: + m["schedule"] = \ + ldb.MessageElement(ndr_pack(self.schedule), + ldb.FLAG_MOD_ADD, "schedule") + try: + samdb.add(m) + except ldb.LdbError, (enum, estr): + raise Exception("Could not add nTDSConnection for (%s) - (%s)" % + (self.dnstr, estr)) + + def commit_modified(self, samdb, ro=False): + """Local helper routine for commit_connections() which + handles committed connections that are to be modified to the + database + """ + assert self.to_be_modified + self.to_be_modified = False + + # No database modification requested + if ro: + return + + # First verify we have this entry to ensure nothing + # is programatically amiss + try: + msg = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE) + found = True + + except ldb.LdbError, (enum, estr): + if enum == ldb.ERR_NO_SUCH_OBJECT: + found = False + else: + raise Exception("Unable to search for (%s) - (%s)" % + (self.dnstr, estr)) + if not found: + raise Exception("nTDSConnection for (%s) doesn't exist!" % + self.dnstr) + + if self.enabled: + enablestr = "TRUE" + else: + enablestr = "FALSE" + + # Prepare a message for modifying the samdb + m = ldb.Message() + m.dn = ldb.Dn(samdb, self.dnstr) + + m["enabledConnection"] = \ + ldb.MessageElement(enablestr, ldb.FLAG_MOD_REPLACE, + "enabledConnection") + m["fromServer"] = \ + ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_REPLACE, + "fromServer") + m["options"] = \ + ldb.MessageElement(str(self.options), ldb.FLAG_MOD_REPLACE, + "options") + m["systemFlags"] = \ + ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_REPLACE, + "systemFlags") + + if self.transport_dnstr is not None: + m["transportType"] = \ + ldb.MessageElement(str(self.transport_dnstr), + ldb.FLAG_MOD_REPLACE, "transportType") + else: + m["transportType"] = \ + ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "transportType") + + if self.schedule is not None: + m["schedule"] = \ + ldb.MessageElement(ndr_pack(self.schedule), + ldb.FLAG_MOD_REPLACE, "schedule") + else: + m["schedule"] = \ + ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "schedule") + try: + samdb.modify(m) + except ldb.LdbError, (enum, estr): + raise Exception("Could not modify nTDSConnection for (%s) - (%s)" % + (self.dnstr, estr)) + + def set_modified(self, truefalse): + self.to_be_modified = truefalse + + def set_added(self, truefalse): + self.to_be_added = truefalse + + def set_deleted(self, truefalse): + self.to_be_deleted = truefalse + + def is_schedule_minimum_once_per_week(self): + """Returns True if our schedule includes at least one + replication interval within the week. False otherwise + """ + if self.schedule is None or self.schedule.dataArray[0] is None: + return False + + for slot in self.schedule.dataArray[0].slots: + if (slot & 0x0F) != 0x0: + return True + return False + + def is_equivalent_schedule(self, sched): + """Returns True if our schedule is equivalent to the input + comparison schedule. + + :param shed: schedule to compare to + """ + if self.schedule is not None: + if sched is None: + return False + elif sched is None: + return True + + if (self.schedule.size != sched.size or + self.schedule.bandwidth != sched.bandwidth or + self.schedule.numberOfSchedules != sched.numberOfSchedules): + return False + + for i, header in enumerate(self.schedule.headerArray): + + if self.schedule.headerArray[i].type != sched.headerArray[i].type: + return False + + if self.schedule.headerArray[i].offset != \ + sched.headerArray[i].offset: + return False + + for a, b in zip(self.schedule.dataArray[i].slots, + sched.dataArray[i].slots): + if a != b: + return False + return True + + def convert_schedule_to_repltimes(self): + """Convert NTDS Connection schedule to replTime schedule. + + NTDS Connection schedule slots are double the size of + the replTime slots but the top portion of the NTDS + Connection schedule slot (4 most significant bits in + uchar) are unused. The 4 least significant bits have + the same (15 minute interval) bit positions as replTimes. + We thus pack two elements of the NTDS Connection schedule + slots into one element of the replTimes slot + If no schedule appears in NTDS Connection then a default + of 0x11 is set in each replTimes slot as per behaviour + noted in a Windows DC. That default would cause replication + within the last 15 minutes of each hour. + """ + times = [0x11] * 84 + + for i, slot in enumerate(times): + if self.schedule is not None and \ + self.schedule.dataArray[0] is not None: + slot = (self.schedule.dataArray[0].slots[i*2] & 0xF) << 4 | \ + (self.schedule.dataArray[0].slots[i*2] & 0xF) + return times + + def is_rodc_topology(self): + """Returns True if NTDS Connection specifies RODC + topology only + """ + if self.options & dsdb.NTDSCONN_OPT_RODC_TOPOLOGY == 0: + return False + return True + + def is_generated(self): + """Returns True if NTDS Connection was generated by the + KCC topology algorithm as opposed to set by the administrator + """ + if self.options & dsdb.NTDSCONN_OPT_IS_GENERATED == 0: + return False + return True + + def is_override_notify_default(self): + """Returns True if NTDS Connection should override notify default + """ + if self.options & dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT == 0: + return False + return True + + def is_use_notify(self): + """Returns True if NTDS Connection should use notify + """ + if self.options & dsdb.NTDSCONN_OPT_USE_NOTIFY == 0: + return False + return True + + def is_twoway_sync(self): + """Returns True if NTDS Connection should use twoway sync + """ + if self.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC == 0: + return False + return True + + def is_intersite_compression_disabled(self): + """Returns True if NTDS Connection intersite compression + is disabled + """ + if self.options & dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION == 0: + return False + return True + + def is_user_owned_schedule(self): + """Returns True if NTDS Connection has a user owned schedule + """ + if self.options & dsdb.NTDSCONN_OPT_USER_OWNED_SCHEDULE == 0: + return False + return True + + def is_enabled(self): + """Returns True if NTDS Connection is enabled + """ + return self.enabled + + def get_from_dnstr(self): + '''Return fromServer dn string attribute''' + return self.from_dnstr + + +class Partition(NamingContext): + """A naming context discovered thru Partitions DN of the config schema. + + This is a more specific form of NamingContext class (inheriting from that + class) and it identifies unique attributes enumerated in the Partitions + such as which nTDSDSAs are cross referenced for replicas + """ + def __init__(self, partstr): + self.partstr = partstr + self.enabled = True + self.system_flags = 0 + self.rw_location_list = [] + self.ro_location_list = [] + + # We don't have enough info to properly + # fill in the naming context yet. We'll get that + # fully set up with load_partition(). + NamingContext.__init__(self, None) + + + def load_partition(self, samdb): + """Given a Partition class object that has been initialized with its + partition dn string, load the partition from the sam database, identify + the type of the partition (schema, domain, etc) and record the list of + nTDSDSAs that appear in the cross reference attributes + msDS-NC-Replica-Locations and msDS-NC-RO-Replica-Locations. + + :param samdb: sam database to load partition from + """ + attrs = [ "nCName", + "Enabled", + "systemFlags", + "msDS-NC-Replica-Locations", + "msDS-NC-RO-Replica-Locations" ] + try: + res = samdb.search(base=self.partstr, scope=ldb.SCOPE_BASE, + attrs=attrs) + + except ldb.LdbError, (enum, estr): + raise Exception("Unable to find partition for (%s) - (%s)" % ( + self.partstr, estr)) + + msg = res[0] + for k in msg.keys(): + if k == "dn": + continue + + if k == "Enabled": + if msg[k][0].upper().lstrip().rstrip() == "TRUE": + self.enabled = True + else: + self.enabled = False + continue + + if k == "systemFlags": + self.system_flags = int(msg[k][0]) + continue + + for value in msg[k]: + dsdn = dsdb_Dn(samdb, value) + dnstr = str(dsdn.dn) + + if k == "nCName": + self.nc_dnstr = dnstr + continue + + if k == "msDS-NC-Replica-Locations": + self.rw_location_list.append(dnstr) + continue + + if k == "msDS-NC-RO-Replica-Locations": + self.ro_location_list.append(dnstr) + continue + + # Now identify what type of NC this partition + # enumerated + self.identify_by_basedn(samdb) + + def is_enabled(self): + """Returns True if partition is enabled + """ + return self.is_enabled + + def is_foreign(self): + """Returns True if this is not an Active Directory NC in our + forest but is instead something else (e.g. a foreign NC) + """ + if (self.system_flags & dsdb.SYSTEM_FLAG_CR_NTDS_NC) == 0: + return True + else: + return False + + def should_be_present(self, target_dsa): + """Tests whether this partition should have an NC replica + on the target dsa. This method returns a tuple of + needed=True/False, ro=True/False, partial=True/False + + :param target_dsa: should NC be present on target dsa + """ + needed = False + ro = False + partial = False + + # If this is the config, schema, or default + # domain NC for the target dsa then it should + # be present + if self.nc_type == NCType.config or \ + self.nc_type == NCType.schema or \ + (self.nc_type == NCType.domain and + self.nc_dnstr == target_dsa.default_dnstr): + needed = True + + # A writable replica of an application NC should be present + # if there a cross reference to the target DSA exists. Depending + # on whether the DSA is ro we examine which type of cross reference + # to look for (msDS-NC-Replica-Locations or + # msDS-NC-RO-Replica-Locations + if self.nc_type == NCType.application: + if target_dsa.is_ro(): + if target_dsa.dsa_dnstr in self.ro_location_list: + needed = True + else: + if target_dsa.dsa_dnstr in self.rw_location_list: + needed = True + + # If the target dsa is a gc then a partial replica of a + # domain NC (other than the DSAs default domain) should exist + # if there is also a cross reference for the DSA + if target_dsa.is_gc() and \ + self.nc_type == NCType.domain and \ + self.nc_dnstr != target_dsa.default_dnstr and \ + (target_dsa.dsa_dnstr in self.ro_location_list or + target_dsa.dsa_dnstr in self.rw_location_list): + needed = True + partial = True + + # partial NCs are always readonly + if needed and (target_dsa.is_ro() or partial): + ro = True + + return needed, ro, partial + + def __str__(self): + '''Debug dump string output of class''' + text = "%s" % NamingContext.__str__(self) + text = text + "\n\tpartdn=%s" % self.partstr + for k in self.rw_location_list: + text = text + "\n\tmsDS-NC-Replica-Locations=%s" % k + for k in self.ro_location_list: + text = text + "\n\tmsDS-NC-RO-Replica-Locations=%s" % k + return text + + +class Site(object): + """An individual site object discovered thru the configuration + naming context. Contains all DSAs that exist within the site + """ + def __init__(self, site_dnstr): + self.site_dnstr = site_dnstr + self.site_options = 0 + self.site_topo_generator = None + self.site_topo_failover = 0 # appears to be in minutes + self.dsa_table = {} + + def load_site(self, samdb): + """Loads the NTDS Site Settions options attribute for the site + as well as querying and loading all DSAs that appear within + the site. + """ + ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr + attrs = ["options", + "interSiteTopologyFailover", + "interSiteTopologyGenerator"] + try: + res = samdb.search(base=ssdn, scope=ldb.SCOPE_BASE, + attrs=attrs) + except ldb.LdbError, (enum, estr): + raise Exception("Unable to find site settings for (%s) - (%s)" % + (ssdn, estr)) + + msg = res[0] + if "options" in msg: + self.site_options = int(msg["options"][0]) + + if "interSiteTopologyGenerator" in msg: + self.site_topo_generator = str(msg["interSiteTopologyGenerator"][0]) + + if "interSiteTopologyFailover" in msg: + self.site_topo_failover = int(msg["interSiteTopologyFailover"][0]) + + self.load_all_dsa(samdb) + + def load_all_dsa(self, samdb): + """Discover all nTDSDSA thru the sites entry and + instantiate and load the DSAs. Each dsa is inserted + into the dsa_table by dn string. + """ + try: + res = samdb.search(self.site_dnstr, + scope=ldb.SCOPE_SUBTREE, + expression="(objectClass=nTDSDSA)") + except ldb.LdbError, (enum, estr): + raise Exception("Unable to find nTDSDSAs - (%s)" % estr) + + for msg in res: + dnstr = str(msg.dn) + + # already loaded + if dnstr in self.dsa_table.keys(): + continue + + dsa = DirectoryServiceAgent(dnstr) + + dsa.load_dsa(samdb) + + # Assign this dsa to my dsa table + # and index by dsa dn + self.dsa_table[dnstr] = dsa + + def get_dsa_by_guidstr(self, guidstr): + for dsa in self.dsa_table.values(): + if str(dsa.dsa_guid) == guidstr: + return dsa + return None + + def get_dsa(self, dnstr): + """Return a previously loaded DSA object by consulting + the sites dsa_table for the provided DSA dn string + + :return: None if DSA doesn't exist + """ + if dnstr in self.dsa_table.keys(): + return self.dsa_table[dnstr] + return None + + def select_istg(self, samdb, mydsa, ro): + """Determine if my DC should be an intersite topology + generator. If my DC is the istg and is both a writeable + DC and the database is opened in write mode then we perform + an originating update to set the interSiteTopologyGenerator + attribute in the NTDS Site Settings object. An RODC always + acts as an ISTG for itself. + """ + # The KCC on an RODC always acts as an ISTG for itself + if mydsa.dsa_is_ro: + mydsa.dsa_is_istg = True + return True + + # Find configuration NC replica for my DSA + for c_rep in mydsa.current_rep_table.values(): + if c_rep.is_config(): + break + + if c_rep is None: + raise Exception("Unable to find config NC replica for (%s)" % + mydsa.dsa_dnstr) + + # Load repsFrom if not already loaded so we can get the current + # state of the config replica and whether we are getting updates + # from the istg + c_rep.load_repsFrom(samdb) + + # From MS-Tech ISTG selection: + # First, the KCC on a writable DC determines whether it acts + # as an ISTG for its site + # + # Let s be the object such that s!lDAPDisplayName = nTDSDSA + # and classSchema in s!objectClass. + # + # Let D be the sequence of objects o in the site of the local + # DC such that o!objectCategory = s. D is sorted in ascending + # order by objectGUID. + # + # Which is a fancy way of saying "sort all the nTDSDSA objects + # in the site by guid in ascending order". Place sorted list + # in D_sort[] + D_sort = [] + d_dsa = None + + unixnow = int(time.time()) # seconds since 1970 + ntnow = unix2nttime(unixnow) # double word number of 100 nanosecond + # intervals since 1600s + + for dsa in self.dsa_table.values(): + D_sort.append(dsa) + + D_sort.sort(sort_dsa_by_guid) + + # Let f be the duration o!interSiteTopologyFailover seconds, or 2 hours + # if o!interSiteTopologyFailover is 0 or has no value. + # + # Note: lastSuccess and ntnow are in 100 nanosecond intervals + # so it appears we have to turn f into the same interval + # + # interSiteTopologyFailover (if set) appears to be in minutes + # so we'll need to convert to senconds and then 100 nanosecond + # intervals + # + # 10,000,000 is number of 100 nanosecond intervals in a second + if self.site_topo_failover == 0: + f = 2 * 60 * 60 * 10000000 + else: + f = self.site_topo_failover * 60 * 10000000 + + # From MS-Tech ISTG selection: + # If o != NULL and o!interSiteTopologyGenerator is not the + # nTDSDSA object for the local DC and + # o!interSiteTopologyGenerator is an element dj of sequence D: + # + if self.site_topo_generator is not None and \ + self.site_topo_generator in self.dsa_table.keys(): + d_dsa = self.dsa_table[self.site_topo_generator] + j_idx = D_sort.index(d_dsa) + + if d_dsa is not None and d_dsa is not mydsa: + # From MS-Tech ISTG selection: + # Let c be the cursor in the replUpToDateVector variable + # associated with the NC replica of the config NC such + # that c.uuidDsa = dj!invocationId. If no such c exists + # (No evidence of replication from current ITSG): + # Let i = j. + # Let t = 0. + # + # Else if the current time < c.timeLastSyncSuccess - f + # (Evidence of time sync problem on current ISTG): + # Let i = 0. + # Let t = 0. + # + # Else (Evidence of replication from current ITSG): + # Let i = j. + # Let t = c.timeLastSyncSuccess. + # + # last_success appears to be a double word containing + # number of 100 nanosecond intervals since the 1600s + if d_dsa.dsa_ivid != c_rep.source_dsa_invocation_id: + i_idx = j_idx + t_time = 0 + + elif ntnow < (c_rep.last_success - f): + i_idx = 0 + t_time = 0 + else: + i_idx = j_idx + t_time = c_rep.last_success + + # Otherwise (Nominate local DC as ISTG): + # Let i be the integer such that di is the nTDSDSA + # object for the local DC. + # Let t = the current time. + else: + i_idx = D_sort.index(mydsa) + t_time = ntnow + + # Compute a function that maintains the current ISTG if + # it is alive, cycles through other candidates if not. + # + # Let k be the integer (i + ((current time - t) / + # o!interSiteTopologyFailover)) MOD |D|. + # + # Note: We don't want to divide by zero here so they must + # have meant "f" instead of "o!interSiteTopologyFailover" + k_idx = (i_idx + ((ntnow - t_time) / f)) % len(D_sort) + + # The local writable DC acts as an ISTG for its site if and + # only if dk is the nTDSDSA object for the local DC. If the + # local DC does not act as an ISTG, the KCC skips the + # remainder of this task. + d_dsa = D_sort[k_idx] + d_dsa.dsa_is_istg = True + + # Update if we are the ISTG, otherwise return + if d_dsa is not mydsa: + return False + + # Nothing to do + if self.site_topo_generator == mydsa.dsa_dnstr: + return True + + self.site_topo_generator = mydsa.dsa_dnstr + + # If readonly database then do not perform a + # persistent update + if ro: + return True + + # Perform update to the samdb + ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr + + m = ldb.Message() + m.dn = ldb.Dn(samdb, ssdn) + + m["interSiteTopologyGenerator"] = \ + ldb.MessageElement(mydsa.dsa_dnstr, ldb.FLAG_MOD_REPLACE, + "interSiteTopologyGenerator") + try: + samdb.modify(m) + + except ldb.LdbError, estr: + raise Exception( + "Could not set interSiteTopologyGenerator for (%s) - (%s)" % + (ssdn, estr)) + return True + + def is_intrasite_topology_disabled(self): + '''Returns True if intra-site topology is disabled for site''' + if (self.site_options & + dsdb.DS_NTDSSETTINGS_OPT_IS_AUTO_TOPOLOGY_DISABLED) != 0: + return True + return False + + def is_intersite_topology_disabled(self): + '''Returns True if inter-site topology is disabled for site''' + if (self.site_options & + dsdb.DS_NTDSSETTINGS_OPT_IS_INTER_SITE_AUTO_TOPOLOGY_DISABLED) != 0: + return True + return False + + def is_random_bridgehead_disabled(self): + '''Returns True if selection of random bridgehead is disabled''' + if (self.site_options & + dsdb.DS_NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED) != 0: + return True + return False + + def is_detect_stale_disabled(self): + '''Returns True if detect stale is disabled for site''' + if (self.site_options & + dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED) != 0: + return True + return False + + def is_cleanup_ntdsconn_disabled(self): + '''Returns True if NTDS Connection cleanup is disabled for site''' + if (self.site_options & + dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED) != 0: + return True + return False + + def same_site(self, dsa): + '''Return True if dsa is in this site''' + if self.get_dsa(dsa.dsa_dnstr): + return True + return False + + def __str__(self): + '''Debug dump string output of class''' + text = "%s:" % self.__class__.__name__ + text = text + "\n\tdn=%s" % self.site_dnstr + text = text + "\n\toptions=0x%X" % self.site_options + text = text + "\n\ttopo_generator=%s" % self.site_topo_generator + text = text + "\n\ttopo_failover=%d" % self.site_topo_failover + for key, dsa in self.dsa_table.items(): + text = text + "\n%s" % dsa + return text + + +class GraphNode(object): + """A graph node describing a set of edges that should be directed to it. + + Each edge is a connection for a particular naming context replica directed + from another node in the forest to this node. + """ + + def __init__(self, dsa_dnstr, max_node_edges): + """Instantiate the graph node according to a DSA dn string + + :param max_node_edges: maximum number of edges that should ever + be directed to the node + """ + self.max_edges = max_node_edges + self.dsa_dnstr = dsa_dnstr + self.edge_from = [] + + def __str__(self): + text = "%s:" % self.__class__.__name__ + text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr + text = text + "\n\tmax_edges=%d" % self.max_edges + + for i, edge in enumerate(self.edge_from): + text = text + "\n\tedge_from[%d]=%s" % (i, edge) + return text + + def add_edge_from(self, from_dsa_dnstr): + """Add an edge from the dsa to our graph nodes edge from list + + :param from_dsa_dnstr: the dsa that the edge emanates from + """ + assert from_dsa_dnstr is not None + + # No edges from myself to myself + if from_dsa_dnstr == self.dsa_dnstr: + return False + # Only one edge from a particular node + if from_dsa_dnstr in self.edge_from: + return False + # Not too many edges + if len(self.edge_from) >= self.max_edges: + return False + self.edge_from.append(from_dsa_dnstr) + return True + + def add_edges_from_connections(self, dsa): + """For each nTDSConnection object associated with a particular + DSA, we test if it implies an edge to this graph node (i.e. + the "fromServer" attribute). If it does then we add an + edge from the server unless we are over the max edges for this + graph node + + :param dsa: dsa with a dnstr equivalent to his graph node + """ + for dnstr, connect in dsa.connect_table.items(): + self.add_edge_from(connect.from_dnstr) + + def add_connections_from_edges(self, dsa): + """For each edge directed to this graph node, ensure there + is a corresponding nTDSConnection object in the dsa. + """ + for edge_dnstr in self.edge_from: + connect = dsa.get_connection_by_from_dnstr(edge_dnstr) + + # For each edge directed to the NC replica that + # "should be present" on the local DC, the KCC determines + # whether an object c exists such that: + # + # c is a child of the DC's nTDSDSA object. + # c.objectCategory = nTDSConnection + # + # Given the NC replica ri from which the edge is directed, + # c.fromServer is the dsname of the nTDSDSA object of + # the DC on which ri "is present". + # + # c.options does not contain NTDSCONN_OPT_RODC_TOPOLOGY + if connect and not connect.is_rodc_topology(): + exists = True + else: + exists = False + + # if no such object exists then the KCC adds an object + # c with the following attributes + if exists: + return + + # Generate a new dnstr for this nTDSConnection + opt = dsdb.NTDSCONN_OPT_IS_GENERATED + flags = dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME + \ + dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE + + dsa.create_connection(opt, flags, None, edge_dnstr, None) + + def has_sufficient_edges(self): + '''Return True if we have met the maximum "from edges" criteria''' + if len(self.edge_from) >= self.max_edges: + return True + return False + + +class Transport(object): + """Class defines a Inter-site transport found under Sites + """ + + def __init__(self, dnstr): + self.dnstr = dnstr + self.options = 0 + self.guid = None + self.name = None + self.address_attr = None + self.bridgehead_list = [] + + def __str__(self): + '''Debug dump string output of Transport object''' + + text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr) + text = text + "\n\tguid=%s" % str(self.guid) + text = text + "\n\toptions=%d" % self.options + text = text + "\n\taddress_attr=%s" % self.address_attr + text = text + "\n\tname=%s" % self.name + for dnstr in self.bridgehead_list: + text = text + "\n\tbridgehead_list=%s" % dnstr + + return text + + def load_transport(self, samdb): + """Given a Transport object with an prior initialization + for the object's DN, search for the DN and load attributes + from the samdb. + """ + attrs = [ "objectGUID", + "options", + "name", + "bridgeheadServerListBL", + "transportAddressAttribute" ] + try: + res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE, + attrs=attrs) + + except ldb.LdbError, (enum, estr): + raise Exception("Unable to find Transport for (%s) - (%s)" % + (self.dnstr, estr)) + + msg = res[0] + self.guid = misc.GUID(samdb.schema_format_value("objectGUID", + msg["objectGUID"][0])) + + if "options" in msg: + self.options = int(msg["options"][0]) + + if "transportAddressAttribute" in msg: + self.address_attr = str(msg["transportAddressAttribute"][0]) + + if "name" in msg: + self.name = str(msg["name"][0]) + + if "bridgeheadServerListBL" in msg: + for value in msg["bridgeheadServerListBL"]: + dsdn = dsdb_Dn(samdb, value) + dnstr = str(dsdn.dn) + if dnstr not in self.bridgehead_list: + self.bridgehead_list.append(dnstr) + + +class RepsFromTo(object): + """Class encapsulation of the NDR repsFromToBlob. + + Removes the necessity of external code having to + understand about other_info or manipulation of + update flags. + """ + def __init__(self, nc_dnstr=None, ndr_blob=None): + + self.__dict__['to_be_deleted'] = False + self.__dict__['nc_dnstr'] = nc_dnstr + self.__dict__['update_flags'] = 0x0 + + # WARNING: + # + # There is a very subtle bug here with python + # and our NDR code. If you assign directly to + # a NDR produced struct (e.g. t_repsFrom.ctr.other_info) + # then a proper python GC reference count is not + # maintained. + # + # To work around this we maintain an internal + # reference to "dns_name(x)" and "other_info" elements + # of repsFromToBlob. This internal reference + # is hidden within this class but it is why you + # see statements like this below: + # + # self.__dict__['ndr_blob'].ctr.other_info = \ + # self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo() + # + # That would appear to be a redundant assignment but + # it is necessary to hold a proper python GC reference + # count. + if ndr_blob is None: + self.__dict__['ndr_blob'] = drsblobs.repsFromToBlob() + self.__dict__['ndr_blob'].version = 0x1 + self.__dict__['dns_name1'] = None + self.__dict__['dns_name2'] = None + + self.__dict__['ndr_blob'].ctr.other_info = \ + self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo() + + else: + self.__dict__['ndr_blob'] = ndr_blob + self.__dict__['other_info'] = ndr_blob.ctr.other_info + + if ndr_blob.version == 0x1: + self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name + self.__dict__['dns_name2'] = None + else: + self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name1 + self.__dict__['dns_name2'] = ndr_blob.ctr.other_info.dns_name2 + + def __str__(self): + '''Debug dump string output of class''' + + text = "%s:" % self.__class__.__name__ + text = text + "\n\tdnstr=%s" % self.nc_dnstr + text = text + "\n\tupdate_flags=0x%X" % self.update_flags + + text = text + "\n\tversion=%d" % self.version + text = text + "\n\tsource_dsa_obj_guid=%s" % \ + str(self.source_dsa_obj_guid) + text = text + "\n\tsource_dsa_invocation_id=%s" % \ + str(self.source_dsa_invocation_id) + text = text + "\n\ttransport_guid=%s" % \ + str(self.transport_guid) + text = text + "\n\treplica_flags=0x%X" % \ + self.replica_flags + text = text + "\n\tconsecutive_sync_failures=%d" % \ + self.consecutive_sync_failures + text = text + "\n\tlast_success=%s" % \ + self.last_success + text = text + "\n\tlast_attempt=%s" % \ + self.last_attempt + text = text + "\n\tdns_name1=%s" % \ + str(self.dns_name1) + text = text + "\n\tdns_name2=%s" % \ + str(self.dns_name2) + text = text + "\n\tschedule[ " + for slot in self.schedule: + text = text + "0x%X " % slot + text = text + "]" + + return text + + def __setattr__(self, item, value): + + if item in [ 'schedule', 'replica_flags', 'transport_guid', + 'source_dsa_obj_guid', 'source_dsa_invocation_id', + 'consecutive_sync_failures', 'last_success', + 'last_attempt' ]: + + if item in ['replica_flags']: + self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_FLAGS + elif item in ['schedule']: + self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE + + setattr(self.__dict__['ndr_blob'].ctr, item, value) + + elif item in ['dns_name1']: + self.__dict__['dns_name1'] = value + + if self.__dict__['ndr_blob'].version == 0x1: + self.__dict__['ndr_blob'].ctr.other_info.dns_name = \ + self.__dict__['dns_name1'] + else: + self.__dict__['ndr_blob'].ctr.other_info.dns_name1 = \ + self.__dict__['dns_name1'] + + elif item in ['dns_name2']: + self.__dict__['dns_name2'] = value + + if self.__dict__['ndr_blob'].version == 0x1: + raise AttributeError(item) + else: + self.__dict__['ndr_blob'].ctr.other_info.dns_name2 = \ + self.__dict__['dns_name2'] + + elif item in ['nc_dnstr']: + self.__dict__['nc_dnstr'] = value + + elif item in ['to_be_deleted']: + self.__dict__['to_be_deleted'] = value + + elif item in ['version']: + raise AttributeError, "Attempt to set readonly attribute %s" % item + else: + raise AttributeError, "Unknown attribute %s" % item + + self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS + + def __getattr__(self, item): + """Overload of RepsFromTo attribute retrieval. + + Allows external code to ignore substructures within the blob + """ + if item in [ 'schedule', 'replica_flags', 'transport_guid', + 'source_dsa_obj_guid', 'source_dsa_invocation_id', + 'consecutive_sync_failures', 'last_success', + 'last_attempt' ]: + return getattr(self.__dict__['ndr_blob'].ctr, item) + + elif item in ['version']: + return self.__dict__['ndr_blob'].version + + elif item in ['dns_name1']: + if self.__dict__['ndr_blob'].version == 0x1: + return self.__dict__['ndr_blob'].ctr.other_info.dns_name + else: + return self.__dict__['ndr_blob'].ctr.other_info.dns_name1 + + elif item in ['dns_name2']: + if self.__dict__['ndr_blob'].version == 0x1: + raise AttributeError(item) + else: + return self.__dict__['ndr_blob'].ctr.other_info.dns_name2 + + elif item in ['to_be_deleted']: + return self.__dict__['to_be_deleted'] + + elif item in ['nc_dnstr']: + return self.__dict__['nc_dnstr'] + + elif item in ['update_flags']: + return self.__dict__['update_flags'] + + raise AttributeError, "Unknwown attribute %s" % item + + def is_modified(self): + return (self.update_flags != 0x0) + + def set_unmodified(self): + self.__dict__['update_flags'] = 0x0 + + +class SiteLink(object): + """Class defines a site link found under sites + """ + + def __init__(self, dnstr): + self.dnstr = dnstr + self.options = 0 + self.system_flags = 0 + self.cost = 0 + self.schedule = None + self.interval = None + self.site_list = [] + + def __str__(self): + '''Debug dump string output of Transport object''' + + text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr) + text = text + "\n\toptions=%d" % self.options + text = text + "\n\tsystem_flags=%d" % self.system_flags + text = text + "\n\tcost=%d" % self.cost + text = text + "\n\tinterval=%s" % self.interval + + if self.schedule is not None: + text = text + "\n\tschedule.size=%s" % self.schedule.size + text = text + "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth + text = text + "\n\tschedule.numberOfSchedules=%s" % \ + self.schedule.numberOfSchedules + + for i, header in enumerate(self.schedule.headerArray): + text = text + "\n\tschedule.headerArray[%d].type=%d" % \ + (i, header.type) + text = text + "\n\tschedule.headerArray[%d].offset=%d" % \ + (i, header.offset) + text = text + "\n\tschedule.dataArray[%d].slots[ " % i + for slot in self.schedule.dataArray[i].slots: + text = text + "0x%X " % slot + text = text + "]" + + for dnstr in self.site_list: + text = text + "\n\tsite_list=%s" % dnstr + return text + + def load_sitelink(self, samdb): + """Given a siteLink object with an prior initialization + for the object's DN, search for the DN and load attributes + from the samdb. + """ + attrs = [ "options", + "systemFlags", + "cost", + "schedule", + "replInterval", + "siteList" ] + try: + res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE, + attrs=attrs) + + except ldb.LdbError, (enum, estr): + raise Exception("Unable to find SiteLink for (%s) - (%s)" % + (self.dnstr, estr)) + + msg = res[0] + + if "options" in msg: + self.options = int(msg["options"][0]) + + if "systemFlags" in msg: + self.system_flags = int(msg["systemFlags"][0]) + + if "cost" in msg: + self.cost = int(msg["cost"][0]) + + if "replInterval" in msg: + self.interval = int(msg["replInterval"][0]) + + if "siteList" in msg: + for value in msg["siteList"]: + dsdn = dsdb_Dn(samdb, value) + dnstr = str(dsdn.dn) + if dnstr not in self.site_list: + self.site_list.append(dnstr) + + def is_sitelink(self, site1_dnstr, site2_dnstr): + """Given a siteLink object, determine if it is a link + between the two input site DNs + """ + if site1_dnstr in self.site_list and site2_dnstr in self.site_list: + return True + return False + + +class VertexColor(object): + (unknown, white, black, red) = range(0, 4) + + +class Vertex(object): + """Class encapsulation of a Site Vertex in the + intersite topology replication algorithm + """ + def __init__(self, site, part): + self.site = site + self.part = part + self.color = VertexColor.unknown + + def color_vertex(self): + """Color each vertex to indicate which kind of NC + replica it contains + """ + # IF s contains one or more DCs with full replicas of the + # NC cr!nCName + # SET v.Color to COLOR.RED + # ELSEIF s contains one or more partial replicas of the NC + # SET v.Color to COLOR.BLACK + #ELSE + # SET v.Color to COLOR.WHITE + + # set to minimum (no replica) + self.color = VertexColor.white + + for dnstr, dsa in self.site.dsa_table.items(): + rep = dsa.get_current_replica(self.part.nc_dnstr) + if rep is None: + continue + + # We have a full replica which is the largest + # value so exit + if not rep.is_partial(): + self.color = VertexColor.red + break + else: + self.color = VertexColor.black + + def is_red(self): + assert(self.color != VertexColor.unknown) + return (self.color == VertexColor.red) + + def is_black(self): + assert(self.color != VertexColor.unknown) + return (self.color == VertexColor.black) + + def is_white(self): + assert(self.color != VertexColor.unknown) + return (self.color == VertexColor.white) + +################################################## +# Global Functions +################################################## +def sort_dsa_by_guid(dsa1, dsa2): + return cmp(dsa1.dsa_guid, dsa2.dsa_guid) diff --git a/python/samba/ms_display_specifiers.py b/python/samba/ms_display_specifiers.py new file mode 100644 index 00000000000..44dfba07b39 --- /dev/null +++ b/python/samba/ms_display_specifiers.py @@ -0,0 +1,187 @@ +# Create DisplaySpecifiers LDIF (as a string) from the documents provided by +# Microsoft under the WSPP. +# +# Copyright (C) Andrew Kroeger <andrew@id10ts.net> 2009 +# +# Based on ms_schema.py +# +# 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 re + +def __read_folded_line(f, buffer): + """Read a line from an LDIF file, unfolding it""" + line = buffer + + while True: + l = f.readline() + + if l[:1] == " ": + # continued line + + # cannot fold an empty line + assert(line != "" and line != "\n") + + # preserves '\n ' + line = line + l + else: + # non-continued line + if line == "": + line = l + + if l == "": + # eof, definitely won't be folded + break + else: + # marks end of a folded line + # line contains the now unfolded line + # buffer contains the start of the next possibly folded line + buffer = l + break + + return (line, buffer) + +# Only compile regexp once. +# Will not match options after the attribute type. +attr_type_re = re.compile("^([A-Za-z][A-Za-z0-9-]*):") + +def __read_raw_entries(f): + """Read an LDIF entry, only unfolding lines""" + + buffer = "" + + while True: + entry = [] + + while True: + (l, buffer) = __read_folded_line(f, buffer) + + if l[:1] == "#": + continue + + if l == "\n" or l == "": + break + + m = attr_type_re.match(l) + + if m: + if l[-1:] == "\n": + l = l[:-1] + + entry.append(l) + else: + print >>sys.stderr, "Invalid line: %s" % l, + sys.exit(1) + + if len(entry): + yield entry + + if l == "": + break + +def fix_dn(dn): + """Fix a string DN to use ${CONFIGDN}""" + + if dn.find("<Configuration NC Distinguished Name>") != -1: + dn = dn.replace("\n ", "") + return dn.replace("<Configuration NC Distinguished Name>", "${CONFIGDN}") + else: + return dn + +def __write_ldif_one(entry): + """Write out entry as LDIF""" + out = [] + + for l in entry: + if l[2] == 0: + out.append("%s: %s" % (l[0], l[1])) + else: + # This is a base64-encoded value + out.append("%s:: %s" % (l[0], l[1])) + + return "\n".join(out) + +def __transform_entry(entry): + """Perform required transformations to the Microsoft-provided LDIF""" + + temp_entry = [] + + for l in entry: + t = [] + + if l.find("::") != -1: + # This is a base64-encoded value + t = l.split(":: ", 1) + t.append(1) + else: + t = l.split(": ", 1) + t.append(0) + + key = t[0].lower() + + if key == "changetype": + continue + + if key == "distinguishedname": + continue + + if key == "instancetype": + continue + + if key == "name": + continue + + if key == "cn": + continue + + if key == "objectcategory": + continue + + if key == "showinadvancedviewonly": + value = t[1].upper().lstrip().rstrip() + if value == "TRUE": + # Remove showInAdvancedViewOnly attribute if it is set to the + # default value of TRUE + continue + + t[1] = fix_dn(t[1]) + + temp_entry.append(t) + + entry = temp_entry + + return entry + +def read_ms_ldif(filename): + """Read and transform Microsoft-provided LDIF file.""" + + out = [] + + f = open(filename, "rU") + for entry in __read_raw_entries(f): + out.append(__write_ldif_one(__transform_entry(entry))) + + return "\n\n".join(out) + "\n\n" + +if __name__ == '__main__': + import sys + + try: + display_specifiers_file = sys.argv[1] + except IndexError: + print >>sys.stderr, "Usage: %s display-specifiers-ldif-file.txt" % (sys.argv[0]) + sys.exit(1) + + print read_ms_ldif(display_specifiers_file) + diff --git a/python/samba/ms_schema.py b/python/samba/ms_schema.py new file mode 100644 index 00000000000..c16693c9b5a --- /dev/null +++ b/python/samba/ms_schema.py @@ -0,0 +1,290 @@ +# create schema.ldif (as a string) from WSPP documentation +# +# based on minschema.py and minschema_wspp +# +# 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/>. + +"""Generate LDIF from WSPP documentation.""" + +import re +import base64 +import uuid + +bitFields = {} + +# ADTS: 2.2.9 +# bit positions as labeled in the docs +bitFields["searchflags"] = { + 'fATTINDEX': 31, # IX + 'fPDNTATTINDEX': 30, # PI + 'fANR': 29, # AR + 'fPRESERVEONDELETE': 28, # PR + 'fCOPY': 27, # CP + 'fTUPLEINDEX': 26, # TP + 'fSUBTREEATTINDEX': 25, # ST + 'fCONFIDENTIAL': 24, # CF + 'fNEVERVALUEAUDIT': 23, # NV + 'fRODCAttribute': 22, # RO + + + # missing in ADTS but required by LDIF + 'fRODCFilteredAttribute': 22, # RO ? + 'fCONFIDENTAIL': 24, # typo + 'fRODCFILTEREDATTRIBUTE': 22 # case + } + +# ADTS: 2.2.10 +bitFields["systemflags"] = { + 'FLAG_ATTR_NOT_REPLICATED': 31, 'FLAG_CR_NTDS_NC': 31, # NR + 'FLAG_ATTR_REQ_PARTIAL_SET_MEMBER': 30, 'FLAG_CR_NTDS_DOMAIN': 30, # PS + 'FLAG_ATTR_IS_CONSTRUCTED': 29, 'FLAG_CR_NTDS_NOT_GC_REPLICATED': 29, # CS + 'FLAG_ATTR_IS_OPERATIONAL': 28, # OP + 'FLAG_SCHEMA_BASE_OBJECT': 27, # BS + 'FLAG_ATTR_IS_RDN': 26, # RD + 'FLAG_DISALLOW_MOVE_ON_DELETE': 6, # DE + 'FLAG_DOMAIN_DISALLOW_MOVE': 5, # DM + 'FLAG_DOMAIN_DISALLOW_RENAME': 4, # DR + 'FLAG_CONFIG_ALLOW_LIMITED_MOVE': 3, # AL + 'FLAG_CONFIG_ALLOW_MOVE': 2, # AM + 'FLAG_CONFIG_ALLOW_RENAME': 1, # AR + 'FLAG_DISALLOW_DELETE': 0 # DD + } + +# ADTS: 2.2.11 +bitFields["schemaflagsex"] = { + 'FLAG_ATTR_IS_CRITICAL': 31 + } + +# ADTS: 3.1.1.2.2.2 +oMObjectClassBER = { + '1.3.12.2.1011.28.0.702' : base64.b64encode('\x2B\x0C\x02\x87\x73\x1C\x00\x85\x3E'), + '1.2.840.113556.1.1.1.12': base64.b64encode('\x2A\x86\x48\x86\xF7\x14\x01\x01\x01\x0C'), + '2.6.6.1.2.5.11.29' : base64.b64encode('\x56\x06\x01\x02\x05\x0B\x1D'), + '1.2.840.113556.1.1.1.11': base64.b64encode('\x2A\x86\x48\x86\xF7\x14\x01\x01\x01\x0B'), + '1.3.12.2.1011.28.0.714' : base64.b64encode('\x2B\x0C\x02\x87\x73\x1C\x00\x85\x4A'), + '1.3.12.2.1011.28.0.732' : base64.b64encode('\x2B\x0C\x02\x87\x73\x1C\x00\x85\x5C'), + '1.2.840.113556.1.1.1.6' : base64.b64encode('\x2A\x86\x48\x86\xF7\x14\x01\x01\x01\x06') +} + +# separated by commas in docs, and must be broken up +multivalued_attrs = set(["auxiliaryclass","maycontain","mustcontain","posssuperiors", + "systemauxiliaryclass","systemmaycontain","systemmustcontain", + "systemposssuperiors"]) + +def __read_folded_line(f, buffer): + """ reads a line from an LDIF file, unfolding it""" + line = buffer + + while True: + l = f.readline() + + if l[:1] == " ": + # continued line + + # cannot fold an empty line + assert(line != "" and line != "\n") + + # preserves '\n ' + line = line + l + else: + # non-continued line + if line == "": + line = l + + if l == "": + # eof, definitely won't be folded + break + else: + # marks end of a folded line + # line contains the now unfolded line + # buffer contains the start of the next possibly folded line + buffer = l + break + + return (line, buffer) + + +def __read_raw_entries(f): + """reads an LDIF entry, only unfolding lines""" + + # will not match options after the attribute type + attr_type_re = re.compile("^([A-Za-z]+[A-Za-z0-9-]*):") + + buffer = "" + + while True: + entry = [] + + while True: + (l, buffer) = __read_folded_line(f, buffer) + + if l[:1] == "#": + continue + + if l == "\n" or l == "": + break + + m = attr_type_re.match(l) + + if m: + if l[-1:] == "\n": + l = l[:-1] + + entry.append(l) + else: + print >>sys.stderr, "Invalid line: %s" % l, + sys.exit(1) + + if len(entry): + yield entry + + if l == "": + break + + +def fix_dn(dn): + """fix a string DN to use ${SCHEMADN}""" + + # folding? + if dn.find("<RootDomainDN>") != -1: + dn = dn.replace("\n ", "") + dn = dn.replace(" ", "") + return dn.replace("CN=Schema,CN=Configuration,<RootDomainDN>", "${SCHEMADN}") + else: + return dn + +def __convert_bitfield(key, value): + """Evaluate the OR expression in 'value'""" + assert(isinstance(value, str)) + + value = value.replace("\n ", "") + value = value.replace(" ", "") + + try: + # some attributes already have numeric values + o = int(value) + except ValueError: + o = 0 + flags = value.split("|") + for f in flags: + bitpos = bitFields[key][f] + o = o | (1 << (31 - bitpos)) + + return str(o) + +def __write_ldif_one(entry): + """Write out entry as LDIF""" + out = [] + + for l in entry: + if isinstance(l[1], str): + vl = [l[1]] + else: + vl = l[1] + + if l[0].lower() == 'omobjectclass': + out.append("%s:: %s" % (l[0], l[1])) + continue + + for v in vl: + out.append("%s: %s" % (l[0], v)) + + + return "\n".join(out) + +def __transform_entry(entry, objectClass): + """Perform transformations required to convert the LDIF-like schema + file entries to LDIF, including Samba-specific stuff.""" + + entry = [l.split(":", 1) for l in entry] + + cn = "" + + for l in entry: + key = l[0].lower() + l[1] = l[1].lstrip() + l[1] = l[1].rstrip() + + if not cn and key == "cn": + cn = l[1] + + if key in multivalued_attrs: + # unlike LDIF, these are comma-separated + l[1] = l[1].replace("\n ", "") + l[1] = l[1].replace(" ", "") + + l[1] = l[1].split(",") + + if key in bitFields: + l[1] = __convert_bitfield(key, l[1]) + + if key == "omobjectclass": + l[1] = oMObjectClassBER[l[1].strip()] + + if isinstance(l[1], str): + l[1] = fix_dn(l[1]) + + + assert(cn) + entry.insert(0, ["dn", "CN=%s,${SCHEMADN}" % cn]) + entry.insert(1, ["objectClass", ["top", objectClass]]) + entry.insert(2, ["cn", cn]) + entry.insert(2, ["objectGUID", str(uuid.uuid4())]) + entry.insert(2, ["adminDescription", cn]) + entry.insert(2, ["adminDisplayName", cn]) + + for l in entry: + key = l[0].lower() + + if key == "cn": + entry.remove(l) + + return entry + +def __parse_schema_file(filename, objectClass): + """Load and transform a schema file.""" + + out = [] + + f = open(filename, "rU") + for entry in __read_raw_entries(f): + out.append(__write_ldif_one(__transform_entry(entry, objectClass))) + + return "\n\n".join(out) + + +def read_ms_schema(attr_file, classes_file, dump_attributes = True, dump_classes = True, debug = False): + """Read WSPP documentation-derived schema files.""" + + attr_ldif = "" + classes_ldif = "" + + if dump_attributes: + attr_ldif = __parse_schema_file(attr_file, "attributeSchema") + if dump_classes: + classes_ldif = __parse_schema_file(classes_file, "classSchema") + + return attr_ldif + "\n\n" + classes_ldif + "\n\n" + +if __name__ == '__main__': + import sys + + try: + attr_file = sys.argv[1] + classes_file = sys.argv[2] + except IndexError: + print >>sys.stderr, "Usage: %s attr-file.txt classes-file.txt" % (sys.argv[0]) + sys.exit(1) + + print read_ms_schema(attr_file, classes_file) diff --git a/python/samba/ndr.py b/python/samba/ndr.py new file mode 100644 index 00000000000..39e4a482efb --- /dev/null +++ b/python/samba/ndr.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- + +# Unix SMB/CIFS implementation. +# Copyright © Jelmer Vernooij <jelmer@samba.org> 2008 +# +# 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/>. +# + + +"""Network Data Representation (NDR) marshalling and unmarshalling.""" + + +def ndr_pack(object): + """Pack a NDR object. + + :param object: Object to pack + :return: String object with marshalled object. + """ + ndr_pack = getattr(object, "__ndr_pack__", None) + if ndr_pack is None: + raise TypeError("%r is not a NDR object" % object) + return ndr_pack() + + +def ndr_unpack(cls, data, allow_remaining=False): + """NDR unpack an object. + + :param cls: Class of the object to unpack + :param data: Buffer to unpack + :param allow_remaining: allows remaining data at the end (default=False) + :return: Unpacked object + """ + object = cls() + object.__ndr_unpack__(data, allow_remaining=allow_remaining) + return object + + +def ndr_print(object): + return object.__ndr_print__() diff --git a/python/samba/netcmd/__init__.py b/python/samba/netcmd/__init__.py new file mode 100644 index 00000000000..a3edf505165 --- /dev/null +++ b/python/samba/netcmd/__init__.py @@ -0,0 +1,231 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2009-2012 +# Copyright (C) Theresa Halloran <theresahalloran@gmail.com> 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 optparse, samba +from samba import getopt as options +from ldb import LdbError +import sys, traceback +import textwrap + +class Option(optparse.Option): + pass + +# This help formatter does text wrapping and preserves newlines +class PlainHelpFormatter(optparse.IndentedHelpFormatter): + def format_description(self,description=""): + desc_width = self.width - self.current_indent + indent = " "*self.current_indent + paragraphs = description.split('\n') + wrapped_paragraphs = [ + textwrap.fill(p, + desc_width, + initial_indent=indent, + subsequent_indent=indent) + for p in paragraphs] + result = "\n".join(wrapped_paragraphs) + "\n" + return result + + def format_epilog(self, epilog): + if epilog: + return "\n" + epilog + "\n" + else: + return "" + +class Command(object): + """A samba-tool command.""" + + def _get_short_description(self): + return self.__doc__.splitlines()[0].rstrip("\n") + + short_description = property(_get_short_description) + + def _get_full_description(self): + lines = self.__doc__.split("\n") + return lines[0] + "\n" + textwrap.dedent("\n".join(lines[1:])) + + full_description = property(_get_full_description) + + def _get_name(self): + name = self.__class__.__name__ + if name.startswith("cmd_"): + return name[4:] + return name + + name = property(_get_name) + + # synopsis must be defined in all subclasses in order to provide the + # command usage + synopsis = None + takes_args = [] + takes_options = [] + takes_optiongroups = {} + + hidden = False + + raw_argv = None + raw_args = None + raw_kwargs = None + + def __init__(self, outf=sys.stdout, errf=sys.stderr): + self.outf = outf + self.errf = errf + + def usage(self, prog, *args): + parser, _ = self._create_parser(prog) + parser.print_usage() + + def show_command_error(self, e): + '''display a command error''' + if isinstance(e, CommandError): + (etype, evalue, etraceback) = e.exception_info + inner_exception = e.inner_exception + message = e.message + force_traceback = False + else: + (etype, evalue, etraceback) = sys.exc_info() + inner_exception = e + message = "uncaught exception" + force_traceback = True + + if isinstance(inner_exception, LdbError): + (ldb_ecode, ldb_emsg) = inner_exception + self.errf.write("ERROR(ldb): %s - %s\n" % (message, ldb_emsg)) + elif isinstance(inner_exception, AssertionError): + self.errf.write("ERROR(assert): %s\n" % message) + force_traceback = True + elif isinstance(inner_exception, RuntimeError): + self.errf.write("ERROR(runtime): %s - %s\n" % (message, evalue)) + elif type(inner_exception) is Exception: + self.errf.write("ERROR(exception): %s - %s\n" % (message, evalue)) + force_traceback = True + elif inner_exception is None: + self.errf.write("ERROR: %s\n" % (message)) + else: + self.errf.write("ERROR(%s): %s - %s\n" % (str(etype), message, evalue)) + force_traceback = True + + if force_traceback or samba.get_debug_level() >= 3: + traceback.print_tb(etraceback) + + def _create_parser(self, prog, epilog=None): + parser = optparse.OptionParser( + usage=self.synopsis, + description=self.full_description, + formatter=PlainHelpFormatter(), + prog=prog,epilog=epilog) + parser.add_options(self.takes_options) + optiongroups = {} + for name, optiongroup in self.takes_optiongroups.iteritems(): + optiongroups[name] = optiongroup(parser) + parser.add_option_group(optiongroups[name]) + return parser, optiongroups + + def message(self, text): + self.outf.write(text+"\n") + + def _run(self, *argv): + parser, optiongroups = self._create_parser(argv[0]) + opts, args = parser.parse_args(list(argv)) + # Filter out options from option groups + args = args[1:] + kwargs = dict(opts.__dict__) + for option_group in parser.option_groups: + for option in option_group.option_list: + if option.dest is not None: + del kwargs[option.dest] + kwargs.update(optiongroups) + + # Check for a min a max number of allowed arguments, whenever possible + # The suffix "?" means zero or one occurence + # The suffix "+" means at least one occurence + min_args = 0 + max_args = 0 + undetermined_max_args = False + for i, arg in enumerate(self.takes_args): + if arg[-1] != "?": + min_args += 1 + if arg[-1] == "+": + undetermined_max_args = True + else: + max_args += 1 + if (len(args) < min_args) or (not undetermined_max_args and len(args) > max_args): + parser.print_usage() + return -1 + + self.raw_argv = list(argv) + self.raw_args = args + self.raw_kwargs = kwargs + + try: + return self.run(*args, **kwargs) + except Exception, e: + self.show_command_error(e) + return -1 + + def run(self): + """Run the command. This should be overriden by all subclasses.""" + raise NotImplementedError(self.run) + + def get_logger(self, name="netcmd"): + """Get a logger object.""" + import logging + logger = logging.getLogger(name) + logger.addHandler(logging.StreamHandler(self.errf)) + return logger + + +class SuperCommand(Command): + """A samba-tool command with subcommands.""" + + synopsis = "%prog <subcommand>" + + subcommands = {} + + def _run(self, myname, subcommand=None, *args): + if subcommand in self.subcommands: + return self.subcommands[subcommand]._run( + "%s %s" % (myname, subcommand), *args) + + epilog = "\nAvailable subcommands:\n" + subcmds = self.subcommands.keys() + subcmds.sort() + max_length = max([len(c) for c in subcmds]) + for cmd_name in subcmds: + cmd = self.subcommands[cmd_name] + if not cmd.hidden: + epilog += " %*s - %s\n" % ( + -max_length, cmd_name, cmd.short_description) + epilog += "For more help on a specific subcommand, please type: %s <subcommand> (-h|--help)\n" % myname + + parser, optiongroups = self._create_parser(myname, epilog=epilog) + args_list = list(args) + if subcommand: + args_list.insert(0, subcommand) + opts, args = parser.parse_args(args_list) + + parser.print_help() + return -1 + + +class CommandError(Exception): + """An exception class for samba-tool Command errors.""" + + def __init__(self, message, inner_exception=None): + self.message = message + self.inner_exception = inner_exception + self.exception_info = sys.exc_info() diff --git a/python/samba/netcmd/common.py b/python/samba/netcmd/common.py new file mode 100644 index 00000000000..5c0bd95f089 --- /dev/null +++ b/python/samba/netcmd/common.py @@ -0,0 +1,71 @@ +# common functions for samba-tool python commands +# +# Copyright Andrew Tridgell 2010 +# Copyright Giampaolo Lauria 2011 <lauria2@yahoo.com> +# +# 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 re +from samba.dcerpc import nbt +from samba.net import Net + + +def _get_user_realm_domain(user): + """ get the realm or the domain and the base user + from user like: + * username + * DOMAIN\username + * username@REALM + """ + baseuser = user + realm = "" + domain = "" + m = re.match(r"(\w+)\\(\w+$)", user) + if m: + domain = m.group(1) + baseuser = m.group(2) + return (baseuser.lower(), domain.upper(), realm) + m = re.match(r"(\w+)@(\w+)", user) + if m: + baseuser = m.group(1) + realm = m.group(2) + return (baseuser.lower(), domain, realm.upper()) + + +def netcmd_dnsname(lp): + '''return the full DNS name of our own host. Used as a default + for hostname when running status queries''' + return lp.get('netbios name').lower() + "." + lp.get('realm').lower() + + +def netcmd_finddc(lp, creds, realm=None): + '''Return domain-name of a writable/ldap-capable DC for the default + domain (parameter "realm" in smb.conf) unless another realm has been + specified as argument''' + net = Net(creds=creds, lp=lp) + if realm is None: + realm = lp.get('realm') + cldap_ret = net.finddc(domain=realm, + flags=nbt.NBT_SERVER_LDAP | nbt.NBT_SERVER_DS | nbt.NBT_SERVER_WRITABLE) + return cldap_ret.pdc_dns_name + + +def netcmd_get_domain_infos_via_cldap(lp, creds, address=None): + '''Return domain informations (CLDAP record) of the ldap-capable + DC with the specified address''' + net = Net(creds=creds, lp=lp) + cldap_ret = net.finddc(address=address, + flags=nbt.NBT_SERVER_LDAP | nbt.NBT_SERVER_DS) + return cldap_ret diff --git a/python/samba/netcmd/dbcheck.py b/python/samba/netcmd/dbcheck.py new file mode 100644 index 00000000000..889b0ff075c --- /dev/null +++ b/python/samba/netcmd/dbcheck.py @@ -0,0 +1,143 @@ +# 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.auth import system_session +from samba.samdb import SamDB +from samba.netcmd import ( + Command, + CommandError, + Option + ) +from samba.dbchecker import dbcheck + + +class cmd_dbcheck(Command): + """Check local AD database for errors.""" + synopsis = "%prog [<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 as a single transaction"), + 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"), + Option("--quiet", dest="quiet", action="store_true", default=False, + help="don't print details of checking"), + Option("--attrs", dest="attrs", default=None, help="list of attributes to check (space separated)"), + Option("--reindex", dest="reindex", default=False, action="store_true", help="force database re-index"), + Option("--force-modules", dest="force_modules", default=False, action="store_true", help="force loading of Samba modules and ignore the @MODULES record (for very old databases)"), + Option("-H", "--URL", help="LDB URL for database or target server (defaults to local SAM database)", + type=str, metavar="URL", dest="H"), + ] + + def run(self, DN=None, H=None, verbose=False, fix=False, yes=False, + cross_ncs=False, quiet=False, + scope="SUB", credopts=None, sambaopts=None, versionopts=None, + attrs=None, reindex=False, force_modules=False): + + lp = sambaopts.get_loadparm() + + over_ldap = H is not None and H.startswith('ldap') + + if over_ldap: + creds = credopts.get_credentials(lp, fallback_machine=True) + else: + creds = None + + if force_modules: + samdb = SamDB(session_info=system_session(), url=H, + credentials=creds, lp=lp, options=["modules=samba_dsdb"]) + else: + try: + samdb = SamDB(session_info=system_session(), url=H, + credentials=creds, lp=lp) + except: + raise CommandError("Failed to connect to DB at %s. If this is a really old sam.ldb (before alpha9), then try again with --force-modules" % H) + + + if H is None or not over_ldap: + samdb_schema = samdb + else: + samdb_schema = SamDB(session_info=system_session(), url=None, + credentials=creds, lp=lp) + + 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) + search_scope = scope_map[scope] + + controls = ['show_deleted:1'] + if over_ldap: + controls.append('paged_results:1:1000') + if cross_ncs: + controls.append("search_options:1:2") + + if not attrs: + attrs = ['*'] + else: + attrs = attrs.split() + + started_transaction = False + if yes and fix: + samdb.transaction_start() + started_transaction = True + try: + chk = dbcheck(samdb, samdb_schema=samdb_schema, verbose=verbose, + fix=fix, yes=yes, quiet=quiet, in_transaction=started_transaction) + + if reindex: + self.outf.write("Re-indexing...\n") + error_count = 0 + if chk.reindex_database(): + self.outf.write("completed re-index OK\n") + + elif force_modules: + self.outf.write("Resetting @MODULES...\n") + error_count = 0 + if chk.reset_modules(): + self.outf.write("completed @MODULES reset OK\n") + + else: + error_count = chk.check_database(DN=DN, scope=search_scope, + controls=controls, attrs=attrs) + except: + if started_transaction: + samdb.transaction_cancel() + raise + + if started_transaction: + samdb.transaction_commit() + + if error_count != 0: + sys.exit(1) diff --git a/python/samba/netcmd/delegation.py b/python/samba/netcmd/delegation.py new file mode 100644 index 00000000000..47dffb07d51 --- /dev/null +++ b/python/samba/netcmd/delegation.py @@ -0,0 +1,263 @@ +# delegation management +# +# Copyright Matthieu Patou mat@samba.org 2010 +# Copyright Stefan Metzmacher metze@samba.org 2011 +# Copyright Bjoern Baumbach bb@sernet.de 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 samba.getopt as options +import ldb +from samba import provision +from samba import dsdb +from samba.samdb import SamDB +from samba.auth import system_session +from samba.netcmd.common import _get_user_realm_domain +from samba.netcmd import ( + Command, + CommandError, + SuperCommand, + Option + ) + + +class cmd_delegation_show(Command): + """Show the delegation setting of an account.""" + + synopsis = "%prog <accountname> [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_args = ["accountname"] + + def run(self, accountname, credopts=None, sambaopts=None, versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + paths = provision.provision_paths_from_lp(lp, lp.get("realm")) + sam = SamDB(paths.samdb, session_info=system_session(), + credentials=creds, lp=lp) + # TODO once I understand how, use the domain info to naildown + # to the correct domain + (cleanedaccount, realm, domain) = _get_user_realm_domain(accountname) + + res = sam.search(expression="sAMAccountName=%s" % + ldb.binary_encode(cleanedaccount), + scope=ldb.SCOPE_SUBTREE, + attrs=["userAccountControl", "msDS-AllowedToDelegateTo"]) + if len(res) == 0: + raise CommandError("Unable to find account name '%s'" % accountname) + assert(len(res) == 1) + + uac = int(res[0].get("userAccountControl")[0]) + allowed = res[0].get("msDS-AllowedToDelegateTo") + + self.outf.write("Account-DN: %s\n" % str(res[0].dn)) + self.outf.write("UF_TRUSTED_FOR_DELEGATION: %s\n" + % bool(uac & dsdb.UF_TRUSTED_FOR_DELEGATION)) + self.outf.write("UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION: %s\n" % + bool(uac & dsdb.UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION)) + + if allowed is not None: + for a in allowed: + self.outf.write("msDS-AllowedToDelegateTo: %s\n" % a) + + +class cmd_delegation_for_any_service(Command): + """Set/unset UF_TRUSTED_FOR_DELEGATION for an account.""" + + synopsis = "%prog <accountname> [(on|off)] [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_args = ["accountname", "onoff"] + + def run(self, accountname, onoff, credopts=None, sambaopts=None, + versionopts=None): + + on = False + if onoff == "on": + on = True + elif onoff == "off": + on = False + else: + raise CommandError("invalid argument: '%s' (choose from 'on', 'off')" % onoff) + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + paths = provision.provision_paths_from_lp(lp, lp.get("realm")) + sam = SamDB(paths.samdb, session_info=system_session(), + credentials=creds, lp=lp) + # TODO once I understand how, use the domain info to naildown + # to the correct domain + (cleanedaccount, realm, domain) = _get_user_realm_domain(accountname) + + search_filter = "sAMAccountName=%s" % ldb.binary_encode(cleanedaccount) + flag = dsdb.UF_TRUSTED_FOR_DELEGATION + try: + sam.toggle_userAccountFlags(search_filter, flag, + flags_str="Trusted-for-Delegation", + on=on, strict=True) + except Exception, err: + raise CommandError(err) + + +class cmd_delegation_for_any_protocol(Command): + """Set/unset UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION (S4U2Proxy) for an account.""" + + synopsis = "%prog <accountname> [(on|off)] [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_args = ["accountname", "onoff"] + + def run(self, accountname, onoff, credopts=None, sambaopts=None, + versionopts=None): + + on = False + if onoff == "on": + on = True + elif onoff == "off": + on = False + else: + raise CommandError("invalid argument: '%s' (choose from 'on', 'off')" % onoff) + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + paths = provision.provision_paths_from_lp(lp, lp.get("realm")) + sam = SamDB(paths.samdb, session_info=system_session(), + credentials=creds, lp=lp) + # TODO once I understand how, use the domain info to naildown + # to the correct domain + (cleanedaccount, realm, domain) = _get_user_realm_domain(accountname) + + search_filter = "sAMAccountName=%s" % ldb.binary_encode(cleanedaccount) + flag = dsdb.UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION + try: + sam.toggle_userAccountFlags(search_filter, flag, + flags_str="Trusted-to-Authenticate-for-Delegation", + on=on, strict=True) + except Exception, err: + raise CommandError(err) + + +class cmd_delegation_add_service(Command): + """Add a service principal as msDS-AllowedToDelegateTo.""" + + synopsis = "%prog <accountname> <principal> [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_args = ["accountname", "principal"] + + def run(self, accountname, principal, credopts=None, sambaopts=None, + versionopts=None): + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + paths = provision.provision_paths_from_lp(lp, lp.get("realm")) + sam = SamDB(paths.samdb, session_info=system_session(), + credentials=creds, lp=lp) + # TODO once I understand how, use the domain info to naildown + # to the correct domain + (cleanedaccount, realm, domain) = _get_user_realm_domain(accountname) + + res = sam.search(expression="sAMAccountName=%s" % + ldb.binary_encode(cleanedaccount), + scope=ldb.SCOPE_SUBTREE, + attrs=["msDS-AllowedToDelegateTo"]) + if len(res) == 0: + raise CommandError("Unable to find account name '%s'" % accountname) + assert(len(res) == 1) + + msg = ldb.Message() + msg.dn = res[0].dn + msg["msDS-AllowedToDelegateTo"] = ldb.MessageElement([principal], + ldb.FLAG_MOD_ADD, + "msDS-AllowedToDelegateTo") + try: + sam.modify(msg) + except Exception, err: + raise CommandError(err) + + +class cmd_delegation_del_service(Command): + """Delete a service principal as msDS-AllowedToDelegateTo.""" + + synopsis = "%prog <accountname> <principal> [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_args = ["accountname", "principal"] + + def run(self, accountname, principal, credopts=None, sambaopts=None, + versionopts=None): + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + paths = provision.provision_paths_from_lp(lp, lp.get("realm")) + sam = SamDB(paths.samdb, session_info=system_session(), + credentials=creds, lp=lp) + # TODO once I understand how, use the domain info to naildown + # to the correct domain + (cleanedaccount, realm, domain) = _get_user_realm_domain(accountname) + + res = sam.search(expression="sAMAccountName=%s" % + ldb.binary_encode(cleanedaccount), + scope=ldb.SCOPE_SUBTREE, + attrs=["msDS-AllowedToDelegateTo"]) + if len(res) == 0: + raise CommandError("Unable to find account name '%s'" % accountname) + assert(len(res) == 1) + + msg = ldb.Message() + msg.dn = res[0].dn + msg["msDS-AllowedToDelegateTo"] = ldb.MessageElement([principal], + ldb.FLAG_MOD_DELETE, + "msDS-AllowedToDelegateTo") + try: + sam.modify(msg) + except Exception, err: + raise CommandError(err) + + +class cmd_delegation(SuperCommand): + """Delegation management.""" + + subcommands = {} + subcommands["show"] = cmd_delegation_show() + subcommands["for-any-service"] = cmd_delegation_for_any_service() + subcommands["for-any-protocol"] = cmd_delegation_for_any_protocol() + subcommands["add-service"] = cmd_delegation_add_service() + subcommands["del-service"] = cmd_delegation_del_service() diff --git a/python/samba/netcmd/dns.py b/python/samba/netcmd/dns.py new file mode 100644 index 00000000000..c00d17ad727 --- /dev/null +++ b/python/samba/netcmd/dns.py @@ -0,0 +1,1186 @@ +# DNS management tool +# +# Copyright (C) Amitay Isaacs 2011-2012 +# +# 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 samba.getopt as options +from struct import pack +from socket import inet_ntoa +import shlex + +from samba.netcmd import ( + Command, + CommandError, + Option, + SuperCommand, + ) +from samba.dcerpc import dnsp, dnsserver + + +def dns_connect(server, lp, creds): + if server.lower() == 'localhost': + server = '127.0.0.1' + binding_str = "ncacn_ip_tcp:%s[sign]" % server + dns_conn = dnsserver.dnsserver(binding_str, lp, creds) + return dns_conn + + +def bool_string(flag): + if flag == 0: + ret = 'FALSE' + elif flag == 1: + ret = 'TRUE' + else: + ret = 'UNKNOWN (0x%x)' % flag + return ret + + +def enum_string(module, enum_defs, value): + ret = None + for e in enum_defs: + if value == getattr(module, e): + ret = e + break + if not ret: + ret = 'UNKNOWN (0x%x)' % value + return ret + + +def bitmap_string(module, bitmap_defs, value): + ret = '' + for b in bitmap_defs: + if value & getattr(module, b): + ret += '%s ' % b + if not ret: + ret = 'NONE' + return ret + + +def boot_method_string(boot_method): + enum_defs = [ 'DNS_BOOT_METHOD_UNINITIALIZED', 'DNS_BOOT_METHOD_FILE', + 'DNS_BOOT_METHOD_REGISTRY', 'DNS_BOOT_METHOD_DIRECTORY' ] + return enum_string(dnsserver, enum_defs, boot_method) + + +def name_check_flag_string(check_flag): + enum_defs = [ 'DNS_ALLOW_RFC_NAMES_ONLY', 'DNS_ALLOW_NONRFC_NAMES', + 'DNS_ALLOW_MULTIBYTE_NAMES', 'DNS_ALLOW_ALL_NAMES' ] + return enum_string(dnsserver, enum_defs, check_flag) + + +def zone_type_string(zone_type): + enum_defs = [ 'DNS_ZONE_TYPE_CACHE', 'DNS_ZONE_TYPE_PRIMARY', + 'DNS_ZONE_TYPE_SECONDARY', 'DNS_ZONE_TYPE_STUB', + 'DNS_ZONE_TYPE_FORWARDER', 'DNS_ZONE_TYPE_SECONDARY_CACHE' ] + return enum_string(dnsp, enum_defs, zone_type) + + +def zone_update_string(zone_update): + enum_defs = [ 'DNS_ZONE_UPDATE_OFF', 'DNS_ZONE_UPDATE_SECURE', + 'DNS_ZONE_UPDATE_SECURE' ] + return enum_string(dnsp, enum_defs, zone_update) + + +def zone_secondary_security_string(security): + enum_defs = [ 'DNS_ZONE_SECSECURE_NO_SECURITY', 'DNS_ZONE_SECSECURE_NS_ONLY', + 'DNS_ZONE_SECSECURE_LIST_ONLY', 'DNS_ZONE_SECSECURE_NO_XFER' ] + return enum_string(dnsserver, enum_defs, security) + + +def zone_notify_level_string(notify_level): + enum_defs = [ 'DNS_ZONE_NOTIFY_OFF', 'DNS_ZONE_NOTIFY_ALL_SECONDARIES', + 'DNS_ZONE_NOTIFY_LIST_ONLY' ] + return enum_string(dnsserver, enum_defs, notify_level) + + +def dp_flags_string(dp_flags): + bitmap_defs = [ 'DNS_DP_AUTOCREATED', 'DNS_DP_LEGACY', 'DNS_DP_DOMAIN_DEFAULT', + 'DNS_DP_FOREST_DEFAULT', 'DNS_DP_ENLISTED', 'DNS_DP_DELETED' ] + return bitmap_string(dnsserver, bitmap_defs, dp_flags) + + +def zone_flags_string(flags): + bitmap_defs = [ 'DNS_RPC_ZONE_PAUSED', 'DNS_RPC_ZONE_SHUTDOWN', + 'DNS_RPC_ZONE_REVERSE', 'DNS_RPC_ZONE_AUTOCREATED', + 'DNS_RPC_ZONE_DSINTEGRATED', 'DNS_RPC_ZONE_AGING', + 'DNS_RPC_ZONE_UPDATE_UNSECURE', 'DNS_RPC_ZONE_UPDATE_SECURE', + 'DNS_RPC_ZONE_READONLY'] + return bitmap_string(dnsserver, bitmap_defs, flags) + + +def ip4_array_string(array): + ret = [] + if not array: + return ret + for i in xrange(array.AddrCount): + addr = '%s' % inet_ntoa(pack('i', array.AddrArray[i])) + ret.append(addr) + return ret + + +def dns_addr_array_string(array): + ret = [] + if not array: + return ret + for i in xrange(array.AddrCount): + if array.AddrArray[i].MaxSa[0] == 0x02: + addr = '%d.%d.%d.%d (%d)' % \ + tuple(array.AddrArray[i].MaxSa[4:8] + [array.AddrArray[i].MaxSa[3]]) + elif array.AddrArray[i].MaxSa[0] == 0x17: + addr = '%x%x:%x%x:%x%x:%x%x:%x%x:%x%x:%x%x:%x%x (%d)' % \ + tuple(array.AddrArray[i].MaxSa[4:20] + [array.AddrArray[i].MaxSa[3]]) + else: + addr = 'UNKNOWN' + ret.append(addr) + return ret + + +def dns_type_flag(rec_type): + rtype = rec_type.upper() + if rtype == 'A': + record_type = dnsp.DNS_TYPE_A + elif rtype == 'AAAA': + record_type = dnsp.DNS_TYPE_AAAA + elif rtype == 'PTR': + record_type = dnsp.DNS_TYPE_PTR + elif rtype == 'NS': + record_type = dnsp.DNS_TYPE_NS + elif rtype == 'CNAME': + record_type = dnsp.DNS_TYPE_CNAME + elif rtype == 'SOA': + record_type = dnsp.DNS_TYPE_SOA + elif rtype == 'MX': + record_type = dnsp.DNS_TYPE_MX + elif rtype == 'SRV': + record_type = dnsp.DNS_TYPE_SRV + elif rtype == 'TXT': + record_type = dnsp.DNS_TYPE_TXT + elif rtype == 'ALL': + record_type = dnsp.DNS_TYPE_ALL + else: + raise CommandError('Unknown type of DNS record %s' % rec_type) + return record_type + + +def dns_client_version(cli_version): + version = cli_version.upper() + if version == 'W2K': + client_version = dnsserver.DNS_CLIENT_VERSION_W2K + elif version == 'DOTNET': + client_version = dnsserver.DNS_CLIENT_VERSION_DOTNET + elif version == 'LONGHORN': + client_version = dnsserver.DNS_CLIENT_VERSION_LONGHORN + else: + raise CommandError('Unknown client version %s' % cli_version) + return client_version + + +def print_serverinfo(outf, typeid, serverinfo): + outf.write(' dwVersion : 0x%x\n' % serverinfo.dwVersion) + outf.write(' fBootMethod : %s\n' % boot_method_string(serverinfo.fBootMethod)) + outf.write(' fAdminConfigured : %s\n' % bool_string(serverinfo.fAdminConfigured)) + outf.write(' fAllowUpdate : %s\n' % bool_string(serverinfo.fAllowUpdate)) + outf.write(' fDsAvailable : %s\n' % bool_string(serverinfo.fDsAvailable)) + outf.write(' pszServerName : %s\n' % serverinfo.pszServerName) + outf.write(' pszDsContainer : %s\n' % serverinfo.pszDsContainer) + + if typeid != dnsserver.DNSSRV_TYPEID_SERVER_INFO: + outf.write(' aipServerAddrs : %s\n' % + ip4_array_string(serverinfo.aipServerAddrs)) + outf.write(' aipListenAddrs : %s\n' % + ip4_array_string(serverinfo.aipListenAddrs)) + outf.write(' aipForwarders : %s\n' % + ip4_array_string(serverinfo.aipForwarders)) + else: + outf.write(' aipServerAddrs : %s\n' % + dns_addr_array_string(serverinfo.aipServerAddrs)) + outf.write(' aipListenAddrs : %s\n' % + dns_addr_array_string(serverinfo.aipListenAddrs)) + outf.write(' aipForwarders : %s\n' % + dns_addr_array_string(serverinfo.aipForwarders)) + + outf.write(' dwLogLevel : %d\n' % serverinfo.dwLogLevel) + outf.write(' dwDebugLevel : %d\n' % serverinfo.dwDebugLevel) + outf.write(' dwForwardTimeout : %d\n' % serverinfo.dwForwardTimeout) + outf.write(' dwRpcPrototol : 0x%x\n' % serverinfo.dwRpcProtocol) + outf.write(' dwNameCheckFlag : %s\n' % name_check_flag_string(serverinfo.dwNameCheckFlag)) + outf.write(' cAddressAnswerLimit : %d\n' % serverinfo.cAddressAnswerLimit) + outf.write(' dwRecursionRetry : %d\n' % serverinfo.dwRecursionRetry) + outf.write(' dwRecursionTimeout : %d\n' % serverinfo.dwRecursionTimeout) + outf.write(' dwMaxCacheTtl : %d\n' % serverinfo.dwMaxCacheTtl) + outf.write(' dwDsPollingInterval : %d\n' % serverinfo.dwDsPollingInterval) + outf.write(' dwScavengingInterval : %d\n' % serverinfo.dwScavengingInterval) + outf.write(' dwDefaultRefreshInterval : %d\n' % serverinfo.dwDefaultRefreshInterval) + outf.write(' dwDefaultNoRefreshInterval : %d\n' % serverinfo.dwDefaultNoRefreshInterval) + outf.write(' fAutoReverseZones : %s\n' % bool_string(serverinfo.fAutoReverseZones)) + outf.write(' fAutoCacheUpdate : %s\n' % bool_string(serverinfo.fAutoCacheUpdate)) + outf.write(' fRecurseAfterForwarding : %s\n' % bool_string(serverinfo.fRecurseAfterForwarding)) + outf.write(' fForwardDelegations : %s\n' % bool_string(serverinfo.fForwardDelegations)) + outf.write(' fNoRecursion : %s\n' % bool_string(serverinfo.fNoRecursion)) + outf.write(' fSecureResponses : %s\n' % bool_string(serverinfo.fSecureResponses)) + outf.write(' fRoundRobin : %s\n' % bool_string(serverinfo.fRoundRobin)) + outf.write(' fLocalNetPriority : %s\n' % bool_string(serverinfo.fLocalNetPriority)) + outf.write(' fBindSecondaries : %s\n' % bool_string(serverinfo.fBindSecondaries)) + outf.write(' fWriteAuthorityNs : %s\n' % bool_string(serverinfo.fWriteAuthorityNs)) + outf.write(' fStrictFileParsing : %s\n' % bool_string(serverinfo.fStrictFileParsing)) + outf.write(' fLooseWildcarding : %s\n' % bool_string(serverinfo.fLooseWildcarding)) + outf.write(' fDefaultAgingState : %s\n' % bool_string(serverinfo.fDefaultAgingState)) + + if typeid != dnsserver.DNSSRV_TYPEID_SERVER_INFO_W2K: + outf.write(' dwRpcStructureVersion : 0x%x\n' % serverinfo.dwRpcStructureVersion) + outf.write(' aipLogFilter : %s\n' % dns_addr_array_string(serverinfo.aipLogFilter)) + outf.write(' pwszLogFilePath : %s\n' % serverinfo.pwszLogFilePath) + outf.write(' pszDomainName : %s\n' % serverinfo.pszDomainName) + outf.write(' pszForestName : %s\n' % serverinfo.pszForestName) + outf.write(' pszDomainDirectoryPartition : %s\n' % serverinfo.pszDomainDirectoryPartition) + outf.write(' pszForestDirectoryPartition : %s\n' % serverinfo.pszForestDirectoryPartition) + + outf.write(' dwLocalNetPriorityNetMask : 0x%x\n' % serverinfo.dwLocalNetPriorityNetMask) + outf.write(' dwLastScavengeTime : %d\n' % serverinfo.dwLastScavengeTime) + outf.write(' dwEventLogLevel : %d\n' % serverinfo.dwEventLogLevel) + outf.write(' dwLogFileMaxSize : %d\n' % serverinfo.dwLogFileMaxSize) + outf.write(' dwDsForestVersion : %d\n' % serverinfo.dwDsForestVersion) + outf.write(' dwDsDomainVersion : %d\n' % serverinfo.dwDsDomainVersion) + outf.write(' dwDsDsaVersion : %d\n' % serverinfo.dwDsDsaVersion) + + if typeid == dnsserver.DNSSRV_TYPEID_SERVER_INFO: + outf.write(' fReadOnlyDC : %s\n' % bool_string(serverinfo.fReadOnlyDC)) + + +def print_zoneinfo(outf, typeid, zoneinfo): + outf.write(' pszZoneName : %s\n' % zoneinfo.pszZoneName) + outf.write(' dwZoneType : %s\n' % zone_type_string(zoneinfo.dwZoneType)) + outf.write(' fReverse : %s\n' % bool_string(zoneinfo.fReverse)) + outf.write(' fAllowUpdate : %s\n' % zone_update_string(zoneinfo.fAllowUpdate)) + outf.write(' fPaused : %s\n' % bool_string(zoneinfo.fPaused)) + outf.write(' fShutdown : %s\n' % bool_string(zoneinfo.fShutdown)) + outf.write(' fAutoCreated : %s\n' % bool_string(zoneinfo.fAutoCreated)) + outf.write(' fUseDatabase : %s\n' % bool_string(zoneinfo.fUseDatabase)) + outf.write(' pszDataFile : %s\n' % zoneinfo.pszDataFile) + if typeid != dnsserver.DNSSRV_TYPEID_ZONE_INFO: + outf.write(' aipMasters : %s\n' % + ip4_array_string(zoneinfo.aipMasters)) + else: + outf.write(' aipMasters : %s\n' % + dns_addr_array_string(zoneinfo.aipMasters)) + outf.write(' fSecureSecondaries : %s\n' % zone_secondary_security_string(zoneinfo.fSecureSecondaries)) + outf.write(' fNotifyLevel : %s\n' % zone_notify_level_string(zoneinfo.fNotifyLevel)) + if typeid != dnsserver.DNSSRV_TYPEID_ZONE_INFO: + outf.write(' aipSecondaries : %s\n' % + ip4_array_string(zoneinfo.aipSecondaries)) + outf.write(' aipNotify : %s\n' % + ip4_array_string(zoneinfo.aipNotify)) + else: + outf.write(' aipSecondaries : %s\n' % + dns_addr_array_string(zoneinfo.aipSecondaries)) + outf.write(' aipNotify : %s\n' % + dns_addr_array_string(zoneinfo.aipNotify)) + outf.write(' fUseWins : %s\n' % bool_string(zoneinfo.fUseWins)) + outf.write(' fUseNbstat : %s\n' % bool_string(zoneinfo.fUseNbstat)) + outf.write(' fAging : %s\n' % bool_string(zoneinfo.fAging)) + outf.write(' dwNoRefreshInterval : %d\n' % zoneinfo.dwNoRefreshInterval) + outf.write(' dwRefreshInterval : %d\n' % zoneinfo.dwRefreshInterval) + outf.write(' dwAvailForScavengeTime : %d\n' % zoneinfo.dwAvailForScavengeTime) + if typeid != dnsserver.DNSSRV_TYPEID_ZONE_INFO: + outf.write(' aipScavengeServers : %s\n' % + ip4_array_string(zoneinfo.aipScavengeServers)) + else: + outf.write(' aipScavengeServers : %s\n' % + dns_addr_array_string(zoneinfo.aipScavengeServers)) + + if typeid != dnsserver.DNSSRV_TYPEID_ZONE_INFO_W2K: + outf.write(' dwRpcStructureVersion : 0x%x\n' % zoneinfo.dwRpcStructureVersion) + outf.write(' dwForwarderTimeout : %d\n' % zoneinfo.dwForwarderTimeout) + outf.write(' fForwarderSlave : %d\n' % zoneinfo.fForwarderSlave) + if typeid != dnsserver.DNSSRV_TYPEID_ZONE_INFO: + outf.write(' aipLocalMasters : %s\n' % + ip4_array_string(zoneinfo.aipLocalMasters)) + else: + outf.write(' aipLocalMasters : %s\n' % + dns_addr_array_string(zoneinfo.aipLocalMasters)) + outf.write(' dwDpFlags : %s\n' % dp_flags_string(zoneinfo.dwDpFlags)) + outf.write(' pszDpFqdn : %s\n' % zoneinfo.pszDpFqdn) + outf.write(' pwszZoneDn : %s\n' % zoneinfo.pwszZoneDn) + outf.write(' dwLastSuccessfulSoaCheck : %d\n' % zoneinfo.dwLastSuccessfulSoaCheck) + outf.write(' dwLastSuccessfulXfr : %d\n' % zoneinfo.dwLastSuccessfulXfr) + + if typeid == dnsserver.DNSSRV_TYPEID_ZONE_INFO: + outf.write(' fQueuedForBackgroundLoad : %s\n' % bool_string(zoneinfo.fQueuedForBackgroundLoad)) + outf.write(' fBackgroundLoadInProgress : %s\n' % bool_string(zoneinfo.fBackgroundLoadInProgress)) + outf.write(' fReadOnlyZone : %s\n' % bool_string(zoneinfo.fReadOnlyZone)) + outf.write(' dwLastXfrAttempt : %d\n' % zoneinfo.dwLastXfrAttempt) + outf.write(' dwLastXfrResult : %d\n' % zoneinfo.dwLastXfrResult) + + +def print_zone(outf, typeid, zone): + outf.write(' pszZoneName : %s\n' % zone.pszZoneName) + outf.write(' Flags : %s\n' % zone_flags_string(zone.Flags)) + outf.write(' ZoneType : %s\n' % zone_type_string(zone.ZoneType)) + outf.write(' Version : %s\n' % zone.Version) + + if typeid != dnsserver.DNSSRV_TYPEID_ZONE_W2K: + outf.write(' dwDpFlags : %s\n' % dp_flags_string(zone.dwDpFlags)) + outf.write(' pszDpFqdn : %s\n' % zone.pszDpFqdn) + + +def print_enumzones(outf, typeid, zones): + outf.write(' %d zone(s) found\n' % zones.dwZoneCount) + for zone in zones.ZoneArray: + outf.write('\n') + print_zone(outf, typeid, zone) + + +def print_dns_record(outf, rec): + if rec.wType == dnsp.DNS_TYPE_A: + mesg = 'A: %s' % (rec.data) + elif rec.wType == dnsp.DNS_TYPE_AAAA: + mesg = 'AAAA: %s' % (rec.data) + elif rec.wType == dnsp.DNS_TYPE_PTR: + mesg = 'PTR: %s' % (rec.data.str) + elif rec.wType == dnsp.DNS_TYPE_NS: + mesg = 'NS: %s' % (rec.data.str) + elif rec.wType == dnsp.DNS_TYPE_CNAME: + mesg = 'CNAME: %s' % (rec.data.str) + elif rec.wType == dnsp.DNS_TYPE_SOA: + mesg = 'SOA: serial=%d, refresh=%d, retry=%d, expire=%d, ns=%s, email=%s' % ( + rec.data.dwSerialNo, + rec.data.dwRefresh, + rec.data.dwRetry, + rec.data.dwExpire, + rec.data.NamePrimaryServer.str, + rec.data.ZoneAdministratorEmail.str) + elif rec.wType == dnsp.DNS_TYPE_MX: + mesg = 'MX: %s (%d)' % (rec.data.nameExchange.str, rec.data.wPreference) + elif rec.wType == dnsp.DNS_TYPE_SRV: + mesg = 'SRV: %s (%d, %d, %d)' % (rec.data.nameTarget.str, rec.data.wPort, + rec.data.wPriority, rec.data.wWeight) + elif rec.wType == dnsp.DNS_TYPE_TXT: + slist = ['"%s"' % name.str for name in rec.data.str] + mesg = 'TXT: %s' % ','.join(slist) + else: + mesg = 'Unknown: ' + outf.write(' %s (flags=%x, serial=%d, ttl=%d)\n' % ( + mesg, rec.dwFlags, rec.dwSerial, rec.dwTtlSeconds)) + + +def print_dnsrecords(outf, records): + for rec in records.rec: + outf.write(' Name=%s, Records=%d, Children=%d\n' % ( + rec.dnsNodeName.str, + rec.wRecordCount, + rec.dwChildCount)) + for dns_rec in rec.records: + print_dns_record(outf, dns_rec) + + +# +# Always create a copy of strings when creating DNS_RPC_RECORDs +# to overcome the bug in pidl generated python bindings. +# + +class ARecord(dnsserver.DNS_RPC_RECORD): + def __init__(self, ip_addr, serial=1, ttl=900, rank=dnsp.DNS_RANK_ZONE, + node_flag=0): + super(ARecord, self).__init__() + self.wType = dnsp.DNS_TYPE_A + self.dwFlags = rank | node_flag + self.dwSerial = serial + self.dwTtlSeconds = ttl + self._ip_addr = ip_addr[:] + self.data = self._ip_addr + + +class AAAARecord(dnsserver.DNS_RPC_RECORD): + + def __init__(self, ip6_addr, serial=1, ttl=900, rank=dnsp.DNS_RANK_ZONE, + node_flag=0): + super(AAAARecord, self).__init__() + self.wType = dnsp.DNS_TYPE_AAAA + self.dwFlags = rank | node_flag + self.dwSerial = serial + self.dwTtlSeconds = ttl + self._ip6_addr = ip6_addr[:] + self.data = self._ip6_addr + + +class PTRRecord(dnsserver.DNS_RPC_RECORD): + + def __init__(self, ptr, serial=1, ttl=900, rank=dnsp.DNS_RANK_ZONE, + node_flag=0): + super(PTRRecord, self).__init__() + self.wType = dnsp.DNS_TYPE_PTR + self.dwFlags = rank | node_flag + self.dwSerial = serial + self.dwTtleSeconds = ttl + self._ptr = ptr[:] + ptr_name = dnsserver.DNS_RPC_NAME() + ptr_name.str = self._ptr + ptr_name.len = len(ptr) + self.data = ptr_name + + +class CNameRecord(dnsserver.DNS_RPC_RECORD): + + def __init__(self, cname, serial=1, ttl=900, rank=dnsp.DNS_RANK_ZONE, + node_flag=0): + super(CNameRecord, self).__init__() + self.wType = dnsp.DNS_TYPE_CNAME + self.dwFlags = rank | node_flag + self.dwSerial = serial + self.dwTtlSeconds = ttl + self._cname = cname[:] + cname_name = dnsserver.DNS_RPC_NAME() + cname_name.str = self._cname + cname_name.len = len(cname) + self.data = cname_name + + +class NSRecord(dnsserver.DNS_RPC_RECORD): + + def __init__(self, dns_server, serial=1, ttl=900, rank=dnsp.DNS_RANK_ZONE, + node_flag=0): + super(NSRecord, self).__init__() + self.wType = dnsp.DNS_TYPE_NS + self.dwFlags = rank | node_flag + self.dwSerial = serial + self.dwTtlSeconds = ttl + self._dns_server = dns_server[:] + ns = dnsserver.DNS_RPC_NAME() + ns.str = self._dns_server + ns.len = len(dns_server) + self.data = ns + + +class MXRecord(dnsserver.DNS_RPC_RECORD): + + def __init__(self, mail_server, preference, serial=1, ttl=900, + rank=dnsp.DNS_RANK_ZONE, node_flag=0): + super(MXRecord, self).__init__() + self.wType = dnsp.DNS_TYPE_MX + self.dwFlags = rank | node_flag + self.dwSerial = serial + self.dwTtlSeconds = ttl + self._mail_server = mail_server[:] + mx = dnsserver.DNS_RPC_RECORD_NAME_PREFERENCE() + mx.wPreference = preference + mx.nameExchange.str = self._mail_server + mx.nameExchange.len = len(mail_server) + self.data = mx + + +class SOARecord(dnsserver.DNS_RPC_RECORD): + + def __init__(self, mname, rname, serial=1, refresh=900, retry=600, + expire=86400, minimum=3600, ttl=3600, rank=dnsp.DNS_RANK_ZONE, + node_flag=dnsp.DNS_RPC_FLAG_AUTH_ZONE_ROOT): + super(SOARecord, self).__init__() + self.wType = dnsp.DNS_TYPE_SOA + self.dwFlags = rank | node_flag + self.dwSerial = serial + self.dwTtlSeconds = ttl + self._mname = mname[:] + self._rname = rname[:] + soa = dnsserver.DNS_RPC_RECORD_SOA() + soa.dwSerialNo = serial + soa.dwRefresh = refresh + soa.dwRetry = retry + soa.dwExpire = expire + soa.NamePrimaryServer.str = self._mname + soa.NamePrimaryServer.len = len(mname) + soa.ZoneAdministratorEmail.str = self._rname + soa.ZoneAdministratorEmail.len = len(rname) + self.data = soa + + +class SRVRecord(dnsserver.DNS_RPC_RECORD): + + def __init__(self, target, port, priority=0, weight=100, serial=1, ttl=900, + rank=dnsp.DNS_RANK_ZONE, node_flag=0): + super(SRVRecord, self).__init__() + self.wType = dnsp.DNS_TYPE_SRV + self.dwFlags = rank | node_flag + self.dwSerial = serial + self.dwTtlSeconds = ttl + self._target = target[:] + srv = dnsserver.DNS_RPC_RECORD_SRV() + srv.wPriority = priority + srv.wWeight = weight + srv.wPort = port + srv.nameTarget.str = self._target + srv.nameTarget.len = len(target) + self.data = srv + + +class TXTRecord(dnsserver.DNS_RPC_RECORD): + + def __init__(self, slist, serial=1, ttl=900, rank=dnsp.DNS_RANK_ZONE, + node_flag=0): + super(TXTRecord, self).__init__() + self.wType = dnsp.DNS_TYPE_TXT + self.dwFlags = rank | node_flag + self.dwSerial = serial + self.dwTtlSeconds = ttl + self._slist = [] + for s in slist: + self._slist.append(s[:]) + names = [] + for s in self._slist: + name = dnsserver.DNS_RPC_NAME() + name.str = s + name.len = len(s) + names.append(name) + txt = dnsserver.DNS_RPC_RECORD_STRING() + txt.count = len(slist) + txt.str = names + self.data = txt + + +# Convert data into a dns record +def data_to_dns_record(record_type, data): + if record_type == dnsp.DNS_TYPE_A: + rec = ARecord(data) + elif record_type == dnsp.DNS_TYPE_AAAA: + rec = AAAARecord(data) + elif record_type == dnsp.DNS_TYPE_PTR: + rec = PTRRecord(data) + elif record_type == dnsp.DNS_TYPE_CNAME: + rec = CNameRecord(data) + elif record_type == dnsp.DNS_TYPE_NS: + rec = NSRecord(data) + elif record_type == dnsp.DNS_TYPE_MX: + tmp = data.split(' ') + if len(tmp) != 2: + raise CommandError('Data requires 2 elements - mail_server, preference') + mail_server = tmp[0] + preference = int(tmp[1]) + rec = MXRecord(mail_server, preference) + elif record_type == dnsp.DNS_TYPE_SRV: + tmp = data.split(' ') + if len(tmp) != 4: + raise CommandError('Data requires 4 elements - server, port, priority, weight') + server = tmp[0] + port = int(tmp[1]) + priority = int(tmp[2]) + weight = int(tmp[3]) + rec = SRVRecord(server, port, priority=priority, weight=weight) + elif record_type == dnsp.DNS_TYPE_SOA: + tmp = data.split(' ') + if len(tmp) != 7: + raise CommandError('Data requires 7 elements - nameserver, email, serial, ' + 'refresh, retry, expire, minimumttl') + nameserver = tmp[0] + email = tmp[1] + serial = int(tmp[2]) + refresh = int(tmp[3]) + retry = int(tmp[4]) + expire = int(tmp[5]) + minimum = int(tmp[6]) + rec = SOARecord(nameserver, email, serial=serial, refresh=refresh, + retry=retry, expire=expire, minimum=minimum) + elif record_type == dnsp.DNS_TYPE_TXT: + slist = shlex.split(data) + rec = TXTRecord(slist) + else: + raise CommandError('Unsupported record type') + return rec + + +# Match dns name (of type DNS_RPC_NAME) +def dns_name_equal(n1, n2): + return n1.str.rstrip('.').lower() == n2.str.rstrip('.').lower() + + +# Match a dns record with specified data +def dns_record_match(dns_conn, server, zone, name, record_type, data): + urec = data_to_dns_record(record_type, data) + + select_flags = dnsserver.DNS_RPC_VIEW_AUTHORITY_DATA + + try: + buflen, res = dns_conn.DnssrvEnumRecords2( + dnsserver.DNS_CLIENT_VERSION_LONGHORN, 0, server, zone, name, None, + record_type, select_flags, None, None) + except RuntimeError, e: + return None + + if not res or res.count == 0: + return None + + rec_match = None + for rec in res.rec[0].records: + if rec.wType != record_type: + continue + + found = False + if record_type == dnsp.DNS_TYPE_A: + if rec.data == urec.data: + found = True + elif record_type == dnsp.DNS_TYPE_AAAA: + if rec.data == urec.data: + found = True + elif record_type == dnsp.DNS_TYPE_PTR: + if dns_name_equal(rec.data, urec.data): + found = True + elif record_type == dnsp.DNS_TYPE_CNAME: + if dns_name_equal(rec.data, urec.data): + found = True + elif record_type == dnsp.DNS_TYPE_NS: + if dns_name_equal(rec.data, urec.data): + found = True + elif record_type == dnsp.DNS_TYPE_MX: + if dns_name_equal(rec.data.nameExchange, urec.data.nameExchange) and \ + rec.data.wPreference == urec.data.wPreference: + found = True + elif record_type == dnsp.DNS_TYPE_SRV: + if rec.data.wPriority == urec.data.wPriority and \ + rec.data.wWeight == urec.data.wWeight and \ + rec.data.wPort == urec.data.wPort and \ + dns_name_equal(rec.data.nameTarget, urec.data.nameTarget): + found = True + elif record_type == dnsp.DNS_TYPE_SOA: + if rec.data.dwSerialNo == urec.data.dwSerialNo and \ + rec.data.dwRefresh == urec.data.dwRefresh and \ + rec.data.dwRetry == urec.data.dwRetry and \ + rec.data.dwExpire == urec.data.dwExpire and \ + rec.data.dwMinimumTtl == urec.data.dwMinimumTtl and \ + dns_name_equal(rec.data.NamePrimaryServer, + urec.data.NamePrimaryServer) and \ + dns_name_equal(rec.data.ZoneAdministratorEmail, + urec.data.ZoneAdministratorEmail): + found = True + elif record_type == dnsp.DNS_TYPE_TXT: + if rec.data.count == urec.data.count: + found = True + for i in xrange(rec.data.count): + found = found and \ + (rec.data.str[i].str == urec.data.str[i].str) + + if found: + rec_match = rec + break + + return rec_match + + +class cmd_serverinfo(Command): + """Query for Server information.""" + + synopsis = '%prog <server> [options]' + + takes_args = [ 'server' ] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option('--client-version', help='Client Version', + default='longhorn', metavar='w2k|dotnet|longhorn', + choices=['w2k','dotnet','longhorn'], dest='cli_ver'), + ] + + def run(self, server, cli_ver, sambaopts=None, credopts=None, + versionopts=None): + self.lp = sambaopts.get_loadparm() + self.creds = credopts.get_credentials(self.lp) + dns_conn = dns_connect(server, self.lp, self.creds) + + client_version = dns_client_version(cli_ver) + + typeid, res = dns_conn.DnssrvQuery2(client_version, 0, server, + None, 'ServerInfo') + print_serverinfo(self.outf, typeid, res) + + +class cmd_zoneinfo(Command): + """Query for zone information.""" + + synopsis = '%prog <server> <zone> [options]' + + takes_args = [ 'server', 'zone' ] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option('--client-version', help='Client Version', + default='longhorn', metavar='w2k|dotnet|longhorn', + choices=['w2k','dotnet','longhorn'], dest='cli_ver'), + ] + + def run(self, server, zone, cli_ver, sambaopts=None, credopts=None, + versionopts=None): + self.lp = sambaopts.get_loadparm() + self.creds = credopts.get_credentials(self.lp) + dns_conn = dns_connect(server, self.lp, self.creds) + + client_version = dns_client_version(cli_ver) + + typeid, res = dns_conn.DnssrvQuery2(client_version, 0, server, zone, + 'ZoneInfo') + print_zoneinfo(self.outf, typeid, res) + + +class cmd_zonelist(Command): + """Query for zones.""" + + synopsis = '%prog <server> [options]' + + takes_args = [ 'server' ] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option('--client-version', help='Client Version', + default='longhorn', metavar='w2k|dotnet|longhorn', + choices=['w2k','dotnet','longhorn'], dest='cli_ver'), + Option('--primary', help='List primary zones (default)', + action='store_true', dest='primary'), + Option('--secondary', help='List secondary zones', + action='store_true', dest='secondary'), + Option('--cache', help='List cached zones', + action='store_true', dest='cache'), + Option('--auto', help='List automatically created zones', + action='store_true', dest='auto'), + Option('--forward', help='List forward zones', + action='store_true', dest='forward'), + Option('--reverse', help='List reverse zones', + action='store_true', dest='reverse'), + Option('--ds', help='List directory integrated zones', + action='store_true', dest='ds'), + Option('--non-ds', help='List non-directory zones', + action='store_true', dest='nonds') + ] + + def run(self, server, cli_ver, primary=False, secondary=False, cache=False, + auto=False, forward=False, reverse=False, ds=False, nonds=False, + sambaopts=None, credopts=None, versionopts=None): + request_filter = 0 + + if primary: + request_filter |= dnsserver.DNS_ZONE_REQUEST_PRIMARY + if secondary: + request_filter |= dnsserver.DNS_ZONE_REQUEST_SECONDARY + if cache: + request_filter |= dnsserver.DNS_ZONE_REQUEST_CACHE + if auto: + request_filter |= dnsserver.DNS_ZONE_REQUEST_AUTO + if forward: + request_filter |= dnsserver.DNS_ZONE_REQUEST_FORWARD + if reverse: + request_filter |= dnsserver.DNS_ZONE_REQUEST_REVERSE + if ds: + request_filter |= dnsserver.DNS_ZONE_REQUEST_DS + if nonds: + request_filter |= dnsserver.DNS_ZONE_REQUEST_NON_DS + + if request_filter == 0: + request_filter = dnsserver.DNS_ZONE_REQUEST_PRIMARY + + self.lp = sambaopts.get_loadparm() + self.creds = credopts.get_credentials(self.lp) + dns_conn = dns_connect(server, self.lp, self.creds) + + client_version = dns_client_version(cli_ver) + + typeid, res = dns_conn.DnssrvComplexOperation2(client_version, + 0, server, None, + 'EnumZones', + dnsserver.DNSSRV_TYPEID_DWORD, + request_filter) + + if client_version == dnsserver.DNS_CLIENT_VERSION_W2K: + typeid = dnsserver.DNSSRV_TYPEID_ZONE_W2K + else: + typeid = dnsserver.DNSSRV_TYPEID_ZONE + print_enumzones(self.outf, typeid, res) + + +class cmd_zonecreate(Command): + """Create a zone.""" + + synopsis = '%prog <server> <zone> [options]' + + takes_args = [ 'server', 'zone' ] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option('--client-version', help='Client Version', + default='longhorn', metavar='w2k|dotnet|longhorn', + choices=['w2k','dotnet','longhorn'], dest='cli_ver') + ] + + def run(self, server, zone, cli_ver, sambaopts=None, credopts=None, + versionopts=None): + + self.lp = sambaopts.get_loadparm() + self.creds = credopts.get_credentials(self.lp) + dns_conn = dns_connect(server, self.lp, self.creds) + + zone = zone.lower() + + client_version = dns_client_version(cli_ver) + if client_version == dnsserver.DNS_CLIENT_VERSION_W2K: + typeid = dnsserver.DNSSRV_TYPEID_ZONE_CREATE_W2K + zone_create_info = dnsserver.DNS_RPC_ZONE_CREATE_INFO_W2K() + zone_create_info.pszZoneName = zone + zone_create_info.dwZoneType = dnsp.DNS_ZONE_TYPE_PRIMARY + zone_create_info.fAllowUpdate = dnsp.DNS_ZONE_UPDATE_SECURE + zone_create_info.fAging = 0 + elif client_version == dnsserver.DNS_CLIENT_VERSION_DOTNET: + typeid = dnsserver.DNSSRV_TYPEID_ZONE_CREATE_DOTNET + zone_create_info = dnsserver.DNS_RPC_ZONE_CREATE_INFO_DOTNET() + zone_create_info.pszZoneName = zone + zone_create_info.dwZoneType = dnsp.DNS_ZONE_TYPE_PRIMARY + zone_create_info.fAllowUpdate = dnsp.DNS_ZONE_UPDATE_SECURE + zone_create_info.fAging = 0 + zone_create_info.dwDpFlags = dnsserver.DNS_DP_DOMAIN_DEFAULT + else: + typeid = dnsserver.DNSSRV_TYPEID_ZONE_CREATE + zone_create_info = dnsserver.DNS_RPC_ZONE_CREATE_INFO_LONGHORN() + zone_create_info.pszZoneName = zone + zone_create_info.dwZoneType = dnsp.DNS_ZONE_TYPE_PRIMARY + zone_create_info.fAllowUpdate = dnsp.DNS_ZONE_UPDATE_SECURE + zone_create_info.fAging = 0 + zone_create_info.dwDpFlags = dnsserver.DNS_DP_DOMAIN_DEFAULT + + res = dns_conn.DnssrvOperation2(client_version, 0, server, None, + 0, 'ZoneCreate', typeid, + zone_create_info) + self.outf.write('Zone %s created successfully\n' % zone) + + +class cmd_zonedelete(Command): + """Delete a zone.""" + + synopsis = '%prog <server> <zone> [options]' + + takes_args = [ 'server', 'zone' ] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + def run(self, server, zone, sambaopts=None, credopts=None, + versionopts=None): + + self.lp = sambaopts.get_loadparm() + self.creds = credopts.get_credentials(self.lp) + dns_conn = dns_connect(server, self.lp, self.creds) + + zone = zone.lower() + res = dns_conn.DnssrvOperation2(dnsserver.DNS_CLIENT_VERSION_LONGHORN, + 0, server, zone, 0, 'DeleteZoneFromDs', + dnsserver.DNSSRV_TYPEID_NULL, + None) + self.outf.write('Zone %s delete successfully\n' % zone) + + +class cmd_query(Command): + """Query a name.""" + + synopsis = '%prog <server> <zone> <name> <A|AAAA|CNAME|MX|NS|SOA|SRV|TXT|ALL> [options]' + + takes_args = [ 'server', 'zone', 'name', 'rtype' ] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option('--authority', help='Search authoritative records (default)', + action='store_true', dest='authority'), + Option('--cache', help='Search cached records', + action='store_true', dest='cache'), + Option('--glue', help='Search glue records', + action='store_true', dest='glue'), + Option('--root', help='Search root hints', + action='store_true', dest='root'), + Option('--additional', help='List additional records', + action='store_true', dest='additional'), + Option('--no-children', help='Do not list children', + action='store_true', dest='no_children'), + Option('--only-children', help='List only children', + action='store_true', dest='only_children') + ] + + def run(self, server, zone, name, rtype, authority=False, cache=False, + glue=False, root=False, additional=False, no_children=False, + only_children=False, sambaopts=None, credopts=None, + versionopts=None): + record_type = dns_type_flag(rtype) + + select_flags = 0 + if authority: + select_flags |= dnsserver.DNS_RPC_VIEW_AUTHORITY_DATA + if cache: + select_flags |= dnsserver.DNS_RPC_VIEW_CACHE_DATA + if glue: + select_flags |= dnsserver.DNS_RPC_VIEW_GLUE_DATA + if root: + select_flags |= dnsserver.DNS_RPC_VIEW_ROOT_HINT_DATA + if additional: + select_flags |= dnsserver.DNS_RPC_VIEW_ADDITIONAL_DATA + if no_children: + select_flags |= dnsserver.DNS_RPC_VIEW_NO_CHILDREN + if only_children: + select_flags |= dnsserver.DNS_RPC_VIEW_ONLY_CHILDREN + + if select_flags == 0: + select_flags = dnsserver.DNS_RPC_VIEW_AUTHORITY_DATA + + if select_flags == dnsserver.DNS_RPC_VIEW_ADDITIONAL_DATA: + self.outf.write('Specify either --authority or --root along with --additional.\n') + self.outf.write('Assuming --authority.\n') + select_flags |= dnsserver.DNS_RPC_VIEW_AUTHORITY_DATA + + self.lp = sambaopts.get_loadparm() + self.creds = credopts.get_credentials(self.lp) + dns_conn = dns_connect(server, self.lp, self.creds) + + buflen, res = dns_conn.DnssrvEnumRecords2( + dnsserver.DNS_CLIENT_VERSION_LONGHORN, 0, server, zone, name, + None, record_type, select_flags, None, None) + print_dnsrecords(self.outf, res) + + +class cmd_roothints(Command): + """Query root hints.""" + + synopsis = '%prog <server> [<name>] [options]' + + takes_args = [ 'server', 'name?' ] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + def run(self, server, name='.', sambaopts=None, credopts=None, + versionopts=None): + record_type = dnsp.DNS_TYPE_NS + select_flags = (dnsserver.DNS_RPC_VIEW_ROOT_HINT_DATA | + dnsserver.DNS_RPC_VIEW_ADDITIONAL_DATA) + + self.lp = sambaopts.get_loadparm() + self.creds = credopts.get_credentials(self.lp) + dns_conn = dns_connect(server, self.lp, self.creds) + + buflen, res = dns_conn.DnssrvEnumRecords2( + dnsserver.DNS_CLIENT_VERSION_LONGHORN, 0, server, '..RootHints', + name, None, record_type, select_flags, None, None) + print_dnsrecords(self.outf, res) + + +class cmd_add_record(Command): + """Add a DNS record + + For each type data contents are as follows: + A ipv4_address_string + AAAA ipv6_address_string + PTR fqdn_string + CNAME fqdn_string + NS fqdn_string + MX "fqdn_string preference" + SRV "fqdn_string port priority weight" + TXT "'string1' 'string2' ..." + """ + + synopsis = '%prog <server> <zone> <name> <A|AAAA|PTR|CNAME|NS|MX|SRV|TXT> <data>' + + takes_args = [ 'server', 'zone', 'name', 'rtype', 'data' ] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + def run(self, server, zone, name, rtype, data, sambaopts=None, + credopts=None, versionopts=None): + + if rtype.upper() not in ('A','AAAA','PTR','CNAME','NS','MX','SRV','TXT'): + raise CommandError('Adding record of type %s is not supported' % rtype) + + record_type = dns_type_flag(rtype) + rec = data_to_dns_record(record_type, data) + + self.lp = sambaopts.get_loadparm() + self.creds = credopts.get_credentials(self.lp) + dns_conn = dns_connect(server, self.lp, self.creds) + + rec_match = dns_record_match(dns_conn, server, zone, name, record_type, + data) + if rec_match is not None: + raise CommandError('Record already exists') + + add_rec_buf = dnsserver.DNS_RPC_RECORD_BUF() + add_rec_buf.rec = rec + + dns_conn.DnssrvUpdateRecord2(dnsserver.DNS_CLIENT_VERSION_LONGHORN, + 0, server, zone, name, add_rec_buf, None) + self.outf.write('Record added successfully\n') + + +class cmd_update_record(Command): + """Update a DNS record + + For each type data contents are as follows: + A ipv4_address_string + AAAA ipv6_address_string + PTR fqdn_string + CNAME fqdn_string + NS fqdn_string + MX "fqdn_string preference" + SRV "fqdn_string port priority weight" + TXT "'string1' 'string2' ..." + """ + + synopsis = '%prog <server> <zone> <name> <A|AAAA|PTR|CNAME|NS|MX|SRV|TXT> <olddata> <newdata>' + + takes_args = [ 'server', 'zone', 'name', 'rtype', 'olddata', 'newdata' ] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + def run(self, server, zone, name, rtype, olddata, newdata, + sambaopts=None, credopts=None, versionopts=None): + + if rtype.upper() not in ('A','AAAA','PTR','CNAME','NS','MX','SRV','TXT'): + raise CommandError('Updating record of type %s is not supported' % rtype) + + record_type = dns_type_flag(rtype) + rec = data_to_dns_record(record_type, newdata) + + self.lp = sambaopts.get_loadparm() + self.creds = credopts.get_credentials(self.lp) + dns_conn = dns_connect(server, self.lp, self.creds) + + rec_match = dns_record_match(dns_conn, server, zone, name, record_type, + olddata) + if not rec_match: + raise CommandError('Record does not exist') + + # Copy properties from existing record to new record + rec.dwFlags = rec_match.dwFlags + rec.dwSerial = rec_match.dwSerial + rec.dwTtlSeconds = rec_match.dwTtlSeconds + rec.dwTimeStamp = rec_match.dwTimeStamp + + add_rec_buf = dnsserver.DNS_RPC_RECORD_BUF() + add_rec_buf.rec = rec + + del_rec_buf = dnsserver.DNS_RPC_RECORD_BUF() + del_rec_buf.rec = rec_match + + dns_conn.DnssrvUpdateRecord2(dnsserver.DNS_CLIENT_VERSION_LONGHORN, + 0, + server, + zone, + name, + add_rec_buf, + del_rec_buf) + self.outf.write('Record updated succefully\n') + + +class cmd_delete_record(Command): + """Delete a DNS record + + For each type data contents are as follows: + A ipv4_address_string + AAAA ipv6_address_string + PTR fqdn_string + CNAME fqdn_string + NS fqdn_string + MX "fqdn_string preference" + SRV "fqdn_string port priority weight" + TXT "'string1' 'string2' ..." + """ + + synopsis = '%prog <server> <zone> <name> <A|AAAA|PTR|CNAME|NS|MX|SRV|TXT> <data>' + + takes_args = [ 'server', 'zone', 'name', 'rtype', 'data' ] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + def run(self, server, zone, name, rtype, data, sambaopts=None, credopts=None, versionopts=None): + + if rtype.upper() not in ('A','AAAA','PTR','CNAME','NS','MX','SRV','TXT'): + raise CommandError('Deleting record of type %s is not supported' % rtype) + + record_type = dns_type_flag(rtype) + + self.lp = sambaopts.get_loadparm() + self.creds = credopts.get_credentials(self.lp) + dns_conn = dns_connect(server, self.lp, self.creds) + + rec_match = dns_record_match(dns_conn, server, zone, name, record_type, data) + if not rec_match: + raise CommandError('Record does not exist') + + del_rec_buf = dnsserver.DNS_RPC_RECORD_BUF() + del_rec_buf.rec = rec_match + + dns_conn.DnssrvUpdateRecord2(dnsserver.DNS_CLIENT_VERSION_LONGHORN, + 0, + server, + zone, + name, + None, + del_rec_buf) + self.outf.write('Record deleted succefully\n') + + +class cmd_dns(SuperCommand): + """Domain Name Service (DNS) management.""" + + subcommands = {} + subcommands['serverinfo'] = cmd_serverinfo() + subcommands['zoneinfo'] = cmd_zoneinfo() + subcommands['zonelist'] = cmd_zonelist() + subcommands['zonecreate'] = cmd_zonecreate() + subcommands['zonedelete'] = cmd_zonedelete() + subcommands['query'] = cmd_query() + subcommands['roothints'] = cmd_roothints() + subcommands['add'] = cmd_add_record() + subcommands['update'] = cmd_update_record() + subcommands['delete'] = cmd_delete_record() diff --git a/python/samba/netcmd/domain.py b/python/samba/netcmd/domain.py new file mode 100644 index 00000000000..4ba305c2713 --- /dev/null +++ b/python/samba/netcmd/domain.py @@ -0,0 +1,1344 @@ +# domain management +# +# Copyright Matthias Dieter Wallnoefer 2009 +# Copyright Andrew Kroeger 2009 +# Copyright Jelmer Vernooij 2007-2012 +# Copyright Giampaolo Lauria 2011 +# Copyright Matthieu Patou <mat@matws.net> 2011 +# Copyright Andrew Bartlett 2008 +# Copyright Stefan Metzmacher 2012 +# +# 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 samba.getopt as options +import ldb +import string +import os +import sys +import tempfile +import logging +from samba.net import Net, LIBNET_JOIN_AUTOMATIC +import samba.ntacls +from samba.join import join_RODC, join_DC, join_subdomain +from samba.auth import system_session +from samba.samdb import SamDB +from samba.dcerpc import drsuapi +from samba.dcerpc.samr import DOMAIN_PASSWORD_COMPLEX, DOMAIN_PASSWORD_STORE_CLEARTEXT +from samba.netcmd import ( + Command, + CommandError, + SuperCommand, + Option + ) +from samba.netcmd.common import netcmd_get_domain_infos_via_cldap +from samba.samba3 import Samba3 +from samba.samba3 import param as s3param +from samba.upgrade import upgrade_from_samba3 +from samba.drs_utils import ( + sendDsReplicaSync, drsuapi_connect, drsException, + sendRemoveDsServer) + + +from samba.dsdb import ( + DS_DOMAIN_FUNCTION_2000, + DS_DOMAIN_FUNCTION_2003, + DS_DOMAIN_FUNCTION_2003_MIXED, + DS_DOMAIN_FUNCTION_2008, + DS_DOMAIN_FUNCTION_2008_R2, + DS_NTDSDSA_OPT_DISABLE_OUTBOUND_REPL, + DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL, + UF_WORKSTATION_TRUST_ACCOUNT, + UF_SERVER_TRUST_ACCOUNT, + UF_TRUSTED_FOR_DELEGATION + ) + +from samba.credentials import DONT_USE_KERBEROS +from samba.provision import ( + provision, + FILL_FULL, + FILL_NT4SYNC, + FILL_DRS, + ProvisioningError, + ) + +def get_testparm_var(testparm, smbconf, varname): + cmd = "%s -s -l --parameter-name='%s' %s 2>/dev/null" % (testparm, varname, smbconf) + output = os.popen(cmd, 'r').readline() + return output.strip() + +try: + import samba.dckeytab + class cmd_domain_export_keytab(Command): + """Dump Kerberos keys of the domain into a keytab.""" + + synopsis = "%prog <keytab> [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_options = [ + Option("--principal", help="extract only this principal", type=str), + ] + + takes_args = ["keytab"] + + def run(self, keytab, credopts=None, sambaopts=None, versionopts=None, principal=None): + lp = sambaopts.get_loadparm() + net = Net(None, lp) + net.export_keytab(keytab=keytab, principal=principal) +except: + cmd_domain_export_keytab = None + + +class cmd_domain_info(Command): + """Print basic info about a domain and the DC passed as parameter.""" + + synopsis = "%prog <ip_address> [options]" + + takes_options = [ + ] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_args = ["address"] + + def run(self, address, credopts=None, sambaopts=None, versionopts=None): + lp = sambaopts.get_loadparm() + try: + res = netcmd_get_domain_infos_via_cldap(lp, None, address) + except RuntimeError: + raise CommandError("Invalid IP address '" + address + "'!") + self.outf.write("Forest : %s\n" % res.forest) + self.outf.write("Domain : %s\n" % res.dns_domain) + self.outf.write("Netbios domain : %s\n" % res.domain_name) + self.outf.write("DC name : %s\n" % res.pdc_dns_name) + self.outf.write("DC netbios name : %s\n" % res.pdc_name) + self.outf.write("Server site : %s\n" % res.server_site) + self.outf.write("Client site : %s\n" % res.client_site) + + +class cmd_domain_provision(Command): + """Provision a domain.""" + + synopsis = "%prog [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("--interactive", help="Ask for names", action="store_true"), + Option("--domain", type="string", metavar="DOMAIN", + help="set domain"), + Option("--domain-guid", type="string", metavar="GUID", + help="set domainguid (otherwise random)"), + Option("--domain-sid", type="string", metavar="SID", + help="set domainsid (otherwise random)"), + Option("--ntds-guid", type="string", metavar="GUID", + help="set NTDS object GUID (otherwise random)"), + Option("--invocationid", type="string", metavar="GUID", + help="set invocationid (otherwise random)"), + Option("--host-name", type="string", metavar="HOSTNAME", + help="set hostname"), + Option("--host-ip", type="string", metavar="IPADDRESS", + help="set IPv4 ipaddress"), + Option("--host-ip6", type="string", metavar="IP6ADDRESS", + help="set IPv6 ipaddress"), + Option("--adminpass", type="string", metavar="PASSWORD", + help="choose admin password (otherwise random)"), + Option("--krbtgtpass", type="string", metavar="PASSWORD", + help="choose krbtgt password (otherwise random)"), + Option("--machinepass", type="string", metavar="PASSWORD", + help="choose machine password (otherwise random)"), + Option("--dns-backend", type="choice", metavar="NAMESERVER-BACKEND", + choices=["SAMBA_INTERNAL", "BIND9_FLATFILE", "BIND9_DLZ", "NONE"], + help="The DNS server backend. SAMBA_INTERNAL is the builtin name server (default), " + "BIND9_FLATFILE uses bind9 text database to store zone information, " + "BIND9_DLZ uses samba4 AD to store zone information, " + "NONE skips the DNS setup entirely (not recommended)", + default="SAMBA_INTERNAL"), + Option("--dnspass", type="string", metavar="PASSWORD", + help="choose dns password (otherwise random)"), + Option("--ldapadminpass", type="string", metavar="PASSWORD", + help="choose password to set between Samba and it's LDAP backend (otherwise random)"), + Option("--root", type="string", metavar="USERNAME", + help="choose 'root' unix username"), + Option("--nobody", type="string", metavar="USERNAME", + help="choose 'nobody' user"), + Option("--users", type="string", metavar="GROUPNAME", + help="choose 'users' group"), + Option("--quiet", help="Be quiet", action="store_true"), + Option("--blank", action="store_true", + help="do not add users or groups, just the structure"), + Option("--ldap-backend-type", type="choice", metavar="LDAP-BACKEND-TYPE", + help="Test initialisation support for unsupported LDAP backend type (fedora-ds or openldap) DO NOT USE", + choices=["fedora-ds", "openldap"]), + Option("--server-role", type="choice", metavar="ROLE", + choices=["domain controller", "dc", "member server", "member", "standalone"], + help="The server role (domain controller | dc | member server | member | standalone). Default is dc.", + default="domain controller"), + Option("--function-level", type="choice", metavar="FOR-FUN-LEVEL", + choices=["2000", "2003", "2008", "2008_R2"], + help="The domain and forest function level (2000 | 2003 | 2008 | 2008_R2 - always native). Default is (Windows) 2003 Native.", + default="2003"), + Option("--next-rid", type="int", metavar="NEXTRID", default=1000, + help="The initial nextRid value (only needed for upgrades). Default is 1000."), + Option("--partitions-only", + help="Configure Samba's partitions, but do not modify them (ie, join a BDC)", action="store_true"), + Option("--targetdir", type="string", metavar="DIR", + help="Set target directory"), + Option("--ol-mmr-urls", type="string", metavar="LDAPSERVER", + help="List of LDAP-URLS [ ldap://<FQHN>:<PORT>/ (where <PORT> has to be different than 389!) ] separated with comma (\",\") for use with OpenLDAP-MMR (Multi-Master-Replication), e.g.: \"ldap://s4dc1:9000,ldap://s4dc2:9000\""), + Option("--use-xattrs", type="choice", choices=["yes", "no", "auto"], help="Define if we should use the native fs capabilities or a tdb file for storing attributes likes ntacl, auto tries to make an inteligent guess based on the user rights and system capabilities", default="auto"), + Option("--use-ntvfs", action="store_true", help="Use NTVFS for the fileserver (default = no)"), + Option("--use-rfc2307", action="store_true", help="Use AD to store posix attributes (default = no)"), + ] + takes_args = [] + + def run(self, sambaopts=None, credopts=None, versionopts=None, + interactive=None, + domain=None, + domain_guid=None, + domain_sid=None, + ntds_guid=None, + invocationid=None, + host_name=None, + host_ip=None, + host_ip6=None, + adminpass=None, + krbtgtpass=None, + machinepass=None, + dns_backend=None, + dns_forwarder=None, + dnspass=None, + ldapadminpass=None, + root=None, + nobody=None, + users=None, + quiet=None, + blank=None, + ldap_backend_type=None, + server_role=None, + function_level=None, + next_rid=None, + partitions_only=None, + targetdir=None, + ol_mmr_urls=None, + use_xattrs=None, + use_ntvfs=None, + use_rfc2307=None): + + self.logger = self.get_logger("provision") + if quiet: + self.logger.setLevel(logging.WARNING) + else: + self.logger.setLevel(logging.INFO) + + lp = sambaopts.get_loadparm() + smbconf = lp.configfile + + creds = credopts.get_credentials(lp) + + creds.set_kerberos_state(DONT_USE_KERBEROS) + + if dns_forwarder is not None: + suggested_forwarder = dns_forwarder + else: + suggested_forwarder = self._get_nameserver_ip() + if suggested_forwarder is None: + suggested_forwarder = "none" + + if len(self.raw_argv) == 1: + interactive = True + + if interactive: + from getpass import getpass + import socket + + def ask(prompt, default=None): + if default is not None: + print "%s [%s]: " % (prompt, default), + else: + print "%s: " % (prompt,), + return sys.stdin.readline().rstrip("\n") or default + + try: + default = socket.getfqdn().split(".", 1)[1].upper() + except IndexError: + default = None + realm = ask("Realm", default) + if realm in (None, ""): + raise CommandError("No realm set!") + + try: + default = realm.split(".")[0] + except IndexError: + default = None + domain = ask("Domain", default) + if domain is None: + raise CommandError("No domain set!") + + server_role = ask("Server Role (dc, member, standalone)", "dc") + + dns_backend = ask("DNS backend (SAMBA_INTERNAL, BIND9_FLATFILE, BIND9_DLZ, NONE)", "SAMBA_INTERNAL") + if dns_backend in (None, ''): + raise CommandError("No DNS backend set!") + + if dns_backend == "SAMBA_INTERNAL": + dns_forwarder = ask("DNS forwarder IP address (write 'none' to disable forwarding)", suggested_forwarder) + if dns_forwarder.lower() in (None, 'none'): + suggested_forwarder = None + dns_forwarder = None + + while True: + adminpassplain = getpass("Administrator password: ") + if not adminpassplain: + self.errf.write("Invalid administrator password.\n") + else: + adminpassverify = getpass("Retype password: ") + if not adminpassplain == adminpassverify: + self.errf.write("Sorry, passwords do not match.\n") + else: + adminpass = adminpassplain + break + + else: + realm = sambaopts._lp.get('realm') + if realm is None: + raise CommandError("No realm set!") + if domain is None: + raise CommandError("No domain set!") + + if not adminpass: + self.logger.info("Administrator password will be set randomly!") + + if function_level == "2000": + dom_for_fun_level = DS_DOMAIN_FUNCTION_2000 + elif function_level == "2003": + dom_for_fun_level = DS_DOMAIN_FUNCTION_2003 + elif function_level == "2008": + dom_for_fun_level = DS_DOMAIN_FUNCTION_2008 + elif function_level == "2008_R2": + dom_for_fun_level = DS_DOMAIN_FUNCTION_2008_R2 + + if dns_backend == "SAMBA_INTERNAL" and dns_forwarder is None: + dns_forwarder = suggested_forwarder + + samdb_fill = FILL_FULL + if blank: + samdb_fill = FILL_NT4SYNC + elif partitions_only: + samdb_fill = FILL_DRS + + if targetdir is not None: + if not os.path.isdir(targetdir): + os.mkdir(targetdir) + + eadb = True + + if use_xattrs == "yes": + eadb = False + elif use_xattrs == "auto" and not lp.get("posix:eadb"): + if targetdir: + file = tempfile.NamedTemporaryFile(dir=os.path.abspath(targetdir)) + else: + file = tempfile.NamedTemporaryFile(dir=os.path.abspath(os.path.dirname(lp.get("private dir")))) + try: + try: + samba.ntacls.setntacl(lp, file.name, + "O:S-1-5-32G:S-1-5-32", "S-1-5-32", "native") + eadb = False + except Exception: + self.logger.info("You are not root or your system do not support xattr, using tdb backend for attributes. ") + finally: + file.close() + + if eadb: + self.logger.info("not using extended attributes to store ACLs and other metadata. If you intend to use this provision in production, rerun the script as root on a system supporting xattrs.") + + session = system_session() + try: + result = provision(self.logger, + session, creds, smbconf=smbconf, targetdir=targetdir, + samdb_fill=samdb_fill, realm=realm, domain=domain, + domainguid=domain_guid, domainsid=domain_sid, + hostname=host_name, + hostip=host_ip, hostip6=host_ip6, + ntdsguid=ntds_guid, + invocationid=invocationid, adminpass=adminpass, + krbtgtpass=krbtgtpass, machinepass=machinepass, + dns_backend=dns_backend, dns_forwarder=dns_forwarder, + dnspass=dnspass, root=root, nobody=nobody, + users=users, + serverrole=server_role, dom_for_fun_level=dom_for_fun_level, + backend_type=ldap_backend_type, + ldapadminpass=ldapadminpass, ol_mmr_urls=ol_mmr_urls, + useeadb=eadb, next_rid=next_rid, lp=lp, use_ntvfs=use_ntvfs, + use_rfc2307=use_rfc2307, skip_sysvolacl=False) + except ProvisioningError, e: + raise CommandError("Provision failed", e) + + result.report_logger(self.logger) + + def _get_nameserver_ip(self): + """Grab the nameserver IP address from /etc/resolv.conf.""" + from os import path + RESOLV_CONF="/etc/resolv.conf" + + if not path.isfile(RESOLV_CONF): + self.logger.warning("Failed to locate %s" % RESOLV_CONF) + return None + + handle = None + try: + handle = open(RESOLV_CONF, 'r') + for line in handle: + if not line.startswith('nameserver'): + continue + # we want the last non-space continuous string of the line + return line.strip().split()[-1] + finally: + if handle is not None: + handle.close() + + self.logger.warning("No nameserver found in %s" % RESOLV_CONF) + + +class cmd_domain_dcpromo(Command): + """Promote an existing domain member or NT4 PDC to an AD DC.""" + + synopsis = "%prog <dnsdomain> [DC|RODC] [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("--server", help="DC to join", type=str), + Option("--site", help="site to join", type=str), + Option("--targetdir", help="where to store provision", type=str), + Option("--domain-critical-only", + help="only replicate critical domain objects", + action="store_true"), + Option("--machinepass", type=str, metavar="PASSWORD", + help="choose machine password (otherwise random)"), + Option("--use-ntvfs", help="Use NTVFS for the fileserver (default = no)", + action="store_true"), + Option("--dns-backend", type="choice", metavar="NAMESERVER-BACKEND", + choices=["SAMBA_INTERNAL", "BIND9_DLZ", "NONE"], + help="The DNS server backend. SAMBA_INTERNAL is the builtin name server (default), " + "BIND9_DLZ uses samba4 AD to store zone information, " + "NONE skips the DNS setup entirely (this DC will not be a DNS server)", + default="SAMBA_INTERNAL") + ] + + takes_args = ["domain", "role?"] + + def run(self, domain, role=None, sambaopts=None, credopts=None, + versionopts=None, server=None, site=None, targetdir=None, + domain_critical_only=False, parent_domain=None, machinepass=None, + use_ntvfs=False, dns_backend=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + net = Net(creds, lp, server=credopts.ipaddress) + + if site is None: + site = "Default-First-Site-Name" + + netbios_name = lp.get("netbios name") + + if not role is None: + role = role.upper() + + if role == "DC": + join_DC(server=server, creds=creds, lp=lp, domain=domain, + site=site, netbios_name=netbios_name, targetdir=targetdir, + domain_critical_only=domain_critical_only, + machinepass=machinepass, use_ntvfs=use_ntvfs, + dns_backend=dns_backend, + promote_existing=True) + elif role == "RODC": + join_RODC(server=server, creds=creds, lp=lp, domain=domain, + site=site, netbios_name=netbios_name, targetdir=targetdir, + domain_critical_only=domain_critical_only, + machinepass=machinepass, use_ntvfs=use_ntvfs, dns_backend=dns_backend, + promote_existing=True) + else: + raise CommandError("Invalid role '%s' (possible values: DC, RODC)" % role) + + +class cmd_domain_join(Command): + """Join domain as either member or backup domain controller.""" + + synopsis = "%prog <dnsdomain> [DC|RODC|MEMBER|SUBDOMAIN] [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("--server", help="DC to join", type=str), + Option("--site", help="site to join", type=str), + Option("--targetdir", help="where to store provision", type=str), + Option("--parent-domain", help="parent domain to create subdomain under", type=str), + Option("--domain-critical-only", + help="only replicate critical domain objects", + action="store_true"), + Option("--machinepass", type=str, metavar="PASSWORD", + help="choose machine password (otherwise random)"), + Option("--use-ntvfs", help="Use NTVFS for the fileserver (default = no)", + action="store_true"), + Option("--dns-backend", type="choice", metavar="NAMESERVER-BACKEND", + choices=["SAMBA_INTERNAL", "BIND9_DLZ", "NONE"], + help="The DNS server backend. SAMBA_INTERNAL is the builtin name server (default), " + "BIND9_DLZ uses samba4 AD to store zone information, " + "NONE skips the DNS setup entirely (this DC will not be a DNS server)", + default="SAMBA_INTERNAL") + ] + + takes_args = ["domain", "role?"] + + def run(self, domain, role=None, sambaopts=None, credopts=None, + versionopts=None, server=None, site=None, targetdir=None, + domain_critical_only=False, parent_domain=None, machinepass=None, + use_ntvfs=False, dns_backend=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + net = Net(creds, lp, server=credopts.ipaddress) + + if site is None: + site = "Default-First-Site-Name" + + netbios_name = lp.get("netbios name") + + if not role is None: + role = role.upper() + + if role is None or role == "MEMBER": + (join_password, sid, domain_name) = net.join_member( + domain, netbios_name, LIBNET_JOIN_AUTOMATIC, + machinepass=machinepass) + + self.errf.write("Joined domain %s (%s)\n" % (domain_name, sid)) + elif role == "DC": + join_DC(server=server, creds=creds, lp=lp, domain=domain, + site=site, netbios_name=netbios_name, targetdir=targetdir, + domain_critical_only=domain_critical_only, + machinepass=machinepass, use_ntvfs=use_ntvfs, dns_backend=dns_backend) + elif role == "RODC": + join_RODC(server=server, creds=creds, lp=lp, domain=domain, + site=site, netbios_name=netbios_name, targetdir=targetdir, + domain_critical_only=domain_critical_only, + machinepass=machinepass, use_ntvfs=use_ntvfs, + dns_backend=dns_backend) + elif role == "SUBDOMAIN": + netbios_domain = lp.get("workgroup") + if parent_domain is None: + parent_domain = ".".join(domain.split(".")[1:]) + join_subdomain(server=server, creds=creds, lp=lp, dnsdomain=domain, + parent_domain=parent_domain, site=site, + netbios_name=netbios_name, netbios_domain=netbios_domain, + targetdir=targetdir, machinepass=machinepass, + use_ntvfs=use_ntvfs, dns_backend=dns_backend) + else: + raise CommandError("Invalid role '%s' (possible values: MEMBER, DC, RODC, SUBDOMAIN)" % role) + + +class cmd_domain_demote(Command): + """Demote ourselves from the role of Domain Controller.""" + + synopsis = "%prog [options]" + + takes_options = [ + Option("--server", help="DC to force replication before demote", type=str), + Option("--targetdir", help="where provision is stored", type=str), + ] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + def run(self, sambaopts=None, credopts=None, + versionopts=None, server=None, targetdir=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + net = Net(creds, lp, server=credopts.ipaddress) + + netbios_name = lp.get("netbios name") + samdb = SamDB(session_info=system_session(), credentials=creds, lp=lp) + if not server: + res = samdb.search(expression='(&(objectClass=computer)(serverReferenceBL=*))', attrs=["dnsHostName", "name"]) + if (len(res) == 0): + raise CommandError("Unable to search for servers") + + if (len(res) == 1): + raise CommandError("You are the latest server in the domain") + + server = None + for e in res: + if str(e["name"]).lower() != netbios_name.lower(): + server = e["dnsHostName"] + break + + ntds_guid = samdb.get_ntds_GUID() + msg = samdb.search(base=str(samdb.get_config_basedn()), + scope=ldb.SCOPE_SUBTREE, expression="(objectGUID=%s)" % ntds_guid, + attrs=['options']) + if len(msg) == 0 or "options" not in msg[0]: + raise CommandError("Failed to find options on %s" % ntds_guid) + + ntds_dn = msg[0].dn + dsa_options = int(str(msg[0]['options'])) + + res = samdb.search(expression="(fSMORoleOwner=%s)" % str(ntds_dn), + controls=["search_options:1:2"]) + + if len(res) != 0: + raise CommandError("Current DC is still the owner of %d role(s), use the role command to transfer roles to another DC" % len(res)) + + self.errf.write("Using %s as partner server for the demotion\n" % + server) + (drsuapiBind, drsuapi_handle, supportedExtensions) = drsuapi_connect(server, lp, creds) + + self.errf.write("Desactivating inbound replication\n") + + nmsg = ldb.Message() + nmsg.dn = msg[0].dn + + dsa_options |= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL + nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options") + samdb.modify(nmsg) + + if not (dsa_options & DS_NTDSDSA_OPT_DISABLE_OUTBOUND_REPL) and not samdb.am_rodc(): + + self.errf.write("Asking partner server %s to synchronize from us\n" + % server) + for part in (samdb.get_schema_basedn(), + samdb.get_config_basedn(), + samdb.get_root_basedn()): + try: + sendDsReplicaSync(drsuapiBind, drsuapi_handle, ntds_guid, str(part), drsuapi.DRSUAPI_DRS_WRIT_REP) + except drsException, e: + self.errf.write( + "Error while demoting, " + "re-enabling inbound replication\n") + dsa_options ^= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL + nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options") + samdb.modify(nmsg) + raise CommandError("Error while sending a DsReplicaSync for partion %s" % str(part), e) + try: + remote_samdb = SamDB(url="ldap://%s" % server, + session_info=system_session(), + credentials=creds, lp=lp) + + self.errf.write("Changing userControl and container\n") + res = remote_samdb.search(base=str(remote_samdb.get_root_basedn()), + expression="(&(objectClass=user)(sAMAccountName=%s$))" % + netbios_name.upper(), + attrs=["userAccountControl"]) + dc_dn = res[0].dn + uac = int(str(res[0]["userAccountControl"])) + + except Exception, e: + self.errf.write( + "Error while demoting, re-enabling inbound replication\n") + dsa_options ^= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL + nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options") + samdb.modify(nmsg) + raise CommandError("Error while changing account control", e) + + if (len(res) != 1): + self.errf.write( + "Error while demoting, re-enabling inbound replication") + dsa_options ^= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL + nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options") + samdb.modify(nmsg) + raise CommandError("Unable to find object with samaccountName = %s$" + " in the remote dc" % netbios_name.upper()) + + olduac = uac + + uac ^= (UF_SERVER_TRUST_ACCOUNT|UF_TRUSTED_FOR_DELEGATION) + uac |= UF_WORKSTATION_TRUST_ACCOUNT + + msg = ldb.Message() + msg.dn = dc_dn + + msg["userAccountControl"] = ldb.MessageElement("%d" % uac, + ldb.FLAG_MOD_REPLACE, + "userAccountControl") + try: + remote_samdb.modify(msg) + except Exception, e: + self.errf.write( + "Error while demoting, re-enabling inbound replication") + dsa_options ^= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL + nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options") + samdb.modify(nmsg) + + raise CommandError("Error while changing account control", e) + + parent = msg.dn.parent() + rdn = str(res[0].dn) + rdn = string.replace(rdn, ",%s" % str(parent), "") + # Let's move to the Computer container + i = 0 + newrdn = rdn + + computer_dn = ldb.Dn(remote_samdb, "CN=Computers,%s" % str(remote_samdb.get_root_basedn())) + res = remote_samdb.search(base=computer_dn, expression=rdn, scope=ldb.SCOPE_ONELEVEL) + + if (len(res) != 0): + res = remote_samdb.search(base=computer_dn, expression="%s-%d" % (rdn, i), + scope=ldb.SCOPE_ONELEVEL) + while(len(res) != 0 and i < 100): + i = i + 1 + res = remote_samdb.search(base=computer_dn, expression="%s-%d" % (rdn, i), + scope=ldb.SCOPE_ONELEVEL) + + if i == 100: + self.errf.write( + "Error while demoting, re-enabling inbound replication\n") + dsa_options ^= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL + nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options") + samdb.modify(nmsg) + + msg = ldb.Message() + msg.dn = dc_dn + + msg["userAccountControl"] = ldb.MessageElement("%d" % uac, + ldb.FLAG_MOD_REPLACE, + "userAccountControl") + + remote_samdb.modify(msg) + + raise CommandError("Unable to find a slot for renaming %s," + " all names from %s-1 to %s-%d seemed used" % + (str(dc_dn), rdn, rdn, i - 9)) + + newrdn = "%s-%d" % (rdn, i) + + try: + newdn = ldb.Dn(remote_samdb, "%s,%s" % (newrdn, str(computer_dn))) + remote_samdb.rename(dc_dn, newdn) + except Exception, e: + self.errf.write( + "Error while demoting, re-enabling inbound replication\n") + dsa_options ^= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL + nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options") + samdb.modify(nmsg) + + msg = ldb.Message() + msg.dn = dc_dn + + msg["userAccountControl"] = ldb.MessageElement("%d" % uac, + ldb.FLAG_MOD_REPLACE, + "userAccountControl") + + remote_samdb.modify(msg) + raise CommandError("Error while renaming %s to %s" % (str(dc_dn), str(newdn)), e) + + + server_dsa_dn = samdb.get_serverName() + domain = remote_samdb.get_root_basedn() + + try: + sendRemoveDsServer(drsuapiBind, drsuapi_handle, server_dsa_dn, domain) + except drsException, e: + self.errf.write( + "Error while demoting, re-enabling inbound replication\n") + dsa_options ^= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL + nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options") + samdb.modify(nmsg) + + msg = ldb.Message() + msg.dn = newdn + + msg["userAccountControl"] = ldb.MessageElement("%d" % uac, + ldb.FLAG_MOD_REPLACE, + "userAccountControl") + print str(dc_dn) + remote_samdb.modify(msg) + remote_samdb.rename(newdn, dc_dn) + raise CommandError("Error while sending a removeDsServer", e) + + for s in ("CN=Entreprise,CN=Microsoft System Volumes,CN=System,CN=Configuration", + "CN=%s,CN=Microsoft System Volumes,CN=System,CN=Configuration" % lp.get("realm"), + "CN=Domain System Volumes (SYSVOL share),CN=File Replication Service,CN=System"): + try: + remote_samdb.delete(ldb.Dn(remote_samdb, + "%s,%s,%s" % (str(rdn), s, str(remote_samdb.get_root_basedn())))) + except ldb.LdbError, l: + pass + + for s in ("CN=Entreprise,CN=NTFRS Subscriptions", + "CN=%s, CN=NTFRS Subscriptions" % lp.get("realm"), + "CN=Domain system Volumes (SYSVOL Share), CN=NTFRS Subscriptions", + "CN=NTFRS Subscriptions"): + try: + remote_samdb.delete(ldb.Dn(remote_samdb, + "%s,%s" % (s, str(newdn)))) + except ldb.LdbError, l: + pass + + self.errf.write("Demote successfull\n") + + +class cmd_domain_level(Command): + """Raise domain and forest function levels.""" + + synopsis = "%prog (show|raise <options>) [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H"), + Option("--quiet", help="Be quiet", action="store_true"), + Option("--forest-level", type="choice", choices=["2003", "2008", "2008_R2"], + help="The forest function level (2003 | 2008 | 2008_R2)"), + Option("--domain-level", type="choice", choices=["2003", "2008", "2008_R2"], + help="The domain function level (2003 | 2008 | 2008_R2)") + ] + + takes_args = ["subcommand"] + + def run(self, subcommand, H=None, forest_level=None, domain_level=None, + quiet=False, credopts=None, sambaopts=None, versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + + domain_dn = samdb.domain_dn() + + res_forest = samdb.search("CN=Partitions,%s" % samdb.get_config_basedn(), + scope=ldb.SCOPE_BASE, attrs=["msDS-Behavior-Version"]) + assert len(res_forest) == 1 + + res_domain = samdb.search(domain_dn, scope=ldb.SCOPE_BASE, + attrs=["msDS-Behavior-Version", "nTMixedDomain"]) + assert len(res_domain) == 1 + + res_dc_s = samdb.search("CN=Sites,%s" % samdb.get_config_basedn(), + scope=ldb.SCOPE_SUBTREE, expression="(objectClass=nTDSDSA)", + attrs=["msDS-Behavior-Version"]) + assert len(res_dc_s) >= 1 + + try: + level_forest = int(res_forest[0]["msDS-Behavior-Version"][0]) + level_domain = int(res_domain[0]["msDS-Behavior-Version"][0]) + level_domain_mixed = int(res_domain[0]["nTMixedDomain"][0]) + + min_level_dc = int(res_dc_s[0]["msDS-Behavior-Version"][0]) # Init value + for msg in res_dc_s: + if int(msg["msDS-Behavior-Version"][0]) < min_level_dc: + min_level_dc = int(msg["msDS-Behavior-Version"][0]) + + if level_forest < 0 or level_domain < 0: + raise CommandError("Domain and/or forest function level(s) is/are invalid. Correct them or reprovision!") + if min_level_dc < 0: + raise CommandError("Lowest function level of a DC is invalid. Correct this or reprovision!") + if level_forest > level_domain: + raise CommandError("Forest function level is higher than the domain level(s). Correct this or reprovision!") + if level_domain > min_level_dc: + raise CommandError("Domain function level is higher than the lowest function level of a DC. Correct this or reprovision!") + + except KeyError: + raise CommandError("Could not retrieve the actual domain, forest level and/or lowest DC function level!") + + if subcommand == "show": + self.message("Domain and forest function level for domain '%s'" % domain_dn) + if level_forest == DS_DOMAIN_FUNCTION_2000 and level_domain_mixed != 0: + self.message("\nATTENTION: You run SAMBA 4 on a forest function level lower than Windows 2000 (Native). This isn't supported! Please raise!") + if level_domain == DS_DOMAIN_FUNCTION_2000 and level_domain_mixed != 0: + self.message("\nATTENTION: You run SAMBA 4 on a domain function level lower than Windows 2000 (Native). This isn't supported! Please raise!") + if min_level_dc == DS_DOMAIN_FUNCTION_2000 and level_domain_mixed != 0: + self.message("\nATTENTION: You run SAMBA 4 on a lowest function level of a DC lower than Windows 2003. This isn't supported! Please step-up or upgrade the concerning DC(s)!") + + self.message("") + + if level_forest == DS_DOMAIN_FUNCTION_2000: + outstr = "2000" + elif level_forest == DS_DOMAIN_FUNCTION_2003_MIXED: + outstr = "2003 with mixed domains/interim (NT4 DC support)" + elif level_forest == DS_DOMAIN_FUNCTION_2003: + outstr = "2003" + elif level_forest == DS_DOMAIN_FUNCTION_2008: + outstr = "2008" + elif level_forest == DS_DOMAIN_FUNCTION_2008_R2: + outstr = "2008 R2" + else: + outstr = "higher than 2008 R2" + self.message("Forest function level: (Windows) " + outstr) + + if level_domain == DS_DOMAIN_FUNCTION_2000 and level_domain_mixed != 0: + outstr = "2000 mixed (NT4 DC support)" + elif level_domain == DS_DOMAIN_FUNCTION_2000 and level_domain_mixed == 0: + outstr = "2000" + elif level_domain == DS_DOMAIN_FUNCTION_2003_MIXED: + outstr = "2003 with mixed domains/interim (NT4 DC support)" + elif level_domain == DS_DOMAIN_FUNCTION_2003: + outstr = "2003" + elif level_domain == DS_DOMAIN_FUNCTION_2008: + outstr = "2008" + elif level_domain == DS_DOMAIN_FUNCTION_2008_R2: + outstr = "2008 R2" + else: + outstr = "higher than 2008 R2" + self.message("Domain function level: (Windows) " + outstr) + + if min_level_dc == DS_DOMAIN_FUNCTION_2000: + outstr = "2000" + elif min_level_dc == DS_DOMAIN_FUNCTION_2003: + outstr = "2003" + elif min_level_dc == DS_DOMAIN_FUNCTION_2008: + outstr = "2008" + elif min_level_dc == DS_DOMAIN_FUNCTION_2008_R2: + outstr = "2008 R2" + else: + outstr = "higher than 2008 R2" + self.message("Lowest function level of a DC: (Windows) " + outstr) + + elif subcommand == "raise": + msgs = [] + + if domain_level is not None: + if domain_level == "2003": + new_level_domain = DS_DOMAIN_FUNCTION_2003 + elif domain_level == "2008": + new_level_domain = DS_DOMAIN_FUNCTION_2008 + elif domain_level == "2008_R2": + new_level_domain = DS_DOMAIN_FUNCTION_2008_R2 + + if new_level_domain <= level_domain and level_domain_mixed == 0: + raise CommandError("Domain function level can't be smaller than or equal to the actual one!") + + if new_level_domain > min_level_dc: + raise CommandError("Domain function level can't be higher than the lowest function level of a DC!") + + # Deactivate mixed/interim domain support + if level_domain_mixed != 0: + # Directly on the base DN + m = ldb.Message() + m.dn = ldb.Dn(samdb, domain_dn) + m["nTMixedDomain"] = ldb.MessageElement("0", + ldb.FLAG_MOD_REPLACE, "nTMixedDomain") + samdb.modify(m) + # Under partitions + m = ldb.Message() + m.dn = ldb.Dn(samdb, "CN=" + lp.get("workgroup") + ",CN=Partitions,%s" % samdb.get_config_basedn()) + m["nTMixedDomain"] = ldb.MessageElement("0", + ldb.FLAG_MOD_REPLACE, "nTMixedDomain") + try: + samdb.modify(m) + except ldb.LdbError, (enum, emsg): + if enum != ldb.ERR_UNWILLING_TO_PERFORM: + raise + + # Directly on the base DN + m = ldb.Message() + m.dn = ldb.Dn(samdb, domain_dn) + m["msDS-Behavior-Version"]= ldb.MessageElement( + str(new_level_domain), ldb.FLAG_MOD_REPLACE, + "msDS-Behavior-Version") + samdb.modify(m) + # Under partitions + m = ldb.Message() + m.dn = ldb.Dn(samdb, "CN=" + lp.get("workgroup") + + ",CN=Partitions,%s" % samdb.get_config_basedn()) + m["msDS-Behavior-Version"]= ldb.MessageElement( + str(new_level_domain), ldb.FLAG_MOD_REPLACE, + "msDS-Behavior-Version") + try: + samdb.modify(m) + except ldb.LdbError, (enum, emsg): + if enum != ldb.ERR_UNWILLING_TO_PERFORM: + raise + + level_domain = new_level_domain + msgs.append("Domain function level changed!") + + if forest_level is not None: + if forest_level == "2003": + new_level_forest = DS_DOMAIN_FUNCTION_2003 + elif forest_level == "2008": + new_level_forest = DS_DOMAIN_FUNCTION_2008 + elif forest_level == "2008_R2": + new_level_forest = DS_DOMAIN_FUNCTION_2008_R2 + if new_level_forest <= level_forest: + raise CommandError("Forest function level can't be smaller than or equal to the actual one!") + if new_level_forest > level_domain: + raise CommandError("Forest function level can't be higher than the domain function level(s). Please raise it/them first!") + m = ldb.Message() + m.dn = ldb.Dn(samdb, "CN=Partitions,%s" % samdb.get_config_basedn()) + m["msDS-Behavior-Version"]= ldb.MessageElement( + str(new_level_forest), ldb.FLAG_MOD_REPLACE, + "msDS-Behavior-Version") + samdb.modify(m) + msgs.append("Forest function level changed!") + msgs.append("All changes applied successfully!") + self.message("\n".join(msgs)) + else: + raise CommandError("invalid argument: '%s' (choose from 'show', 'raise')" % subcommand) + + +class cmd_domain_passwordsettings(Command): + """Set password settings. + + Password complexity, history length, minimum password length, the minimum + and maximum password age) on a Samba4 server. + """ + + synopsis = "%prog (show|set <options>) [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H"), + Option("--quiet", help="Be quiet", action="store_true"), + Option("--complexity", type="choice", choices=["on","off","default"], + help="The password complexity (on | off | default). Default is 'on'"), + Option("--store-plaintext", type="choice", choices=["on","off","default"], + help="Store plaintext passwords where account have 'store passwords with reversible encryption' set (on | off | default). Default is 'off'"), + Option("--history-length", + help="The password history length (<integer> | default). Default is 24.", type=str), + Option("--min-pwd-length", + help="The minimum password length (<integer> | default). Default is 7.", type=str), + Option("--min-pwd-age", + help="The minimum password age (<integer in days> | default). Default is 1.", type=str), + Option("--max-pwd-age", + help="The maximum password age (<integer in days> | default). Default is 43.", type=str), + ] + + takes_args = ["subcommand"] + + def run(self, subcommand, H=None, min_pwd_age=None, max_pwd_age=None, + quiet=False, complexity=None, store_plaintext=None, history_length=None, + min_pwd_length=None, credopts=None, sambaopts=None, + versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + + domain_dn = samdb.domain_dn() + res = samdb.search(domain_dn, scope=ldb.SCOPE_BASE, + attrs=["pwdProperties", "pwdHistoryLength", "minPwdLength", + "minPwdAge", "maxPwdAge"]) + assert(len(res) == 1) + try: + pwd_props = int(res[0]["pwdProperties"][0]) + pwd_hist_len = int(res[0]["pwdHistoryLength"][0]) + cur_min_pwd_len = int(res[0]["minPwdLength"][0]) + # ticks -> days + cur_min_pwd_age = int(abs(int(res[0]["minPwdAge"][0])) / (1e7 * 60 * 60 * 24)) + if int(res[0]["maxPwdAge"][0]) == -0x8000000000000000: + cur_max_pwd_age = 0 + else: + cur_max_pwd_age = int(abs(int(res[0]["maxPwdAge"][0])) / (1e7 * 60 * 60 * 24)) + except Exception, e: + raise CommandError("Could not retrieve password properties!", e) + + if subcommand == "show": + self.message("Password informations for domain '%s'" % domain_dn) + self.message("") + if pwd_props & DOMAIN_PASSWORD_COMPLEX != 0: + self.message("Password complexity: on") + else: + self.message("Password complexity: off") + if pwd_props & DOMAIN_PASSWORD_STORE_CLEARTEXT != 0: + self.message("Store plaintext passwords: on") + else: + self.message("Store plaintext passwords: off") + self.message("Password history length: %d" % pwd_hist_len) + self.message("Minimum password length: %d" % cur_min_pwd_len) + self.message("Minimum password age (days): %d" % cur_min_pwd_age) + self.message("Maximum password age (days): %d" % cur_max_pwd_age) + elif subcommand == "set": + msgs = [] + m = ldb.Message() + m.dn = ldb.Dn(samdb, domain_dn) + + if complexity is not None: + if complexity == "on" or complexity == "default": + pwd_props = pwd_props | DOMAIN_PASSWORD_COMPLEX + msgs.append("Password complexity activated!") + elif complexity == "off": + pwd_props = pwd_props & (~DOMAIN_PASSWORD_COMPLEX) + msgs.append("Password complexity deactivated!") + + if store_plaintext is not None: + if store_plaintext == "on" or store_plaintext == "default": + pwd_props = pwd_props | DOMAIN_PASSWORD_STORE_CLEARTEXT + msgs.append("Plaintext password storage for changed passwords activated!") + elif store_plaintext == "off": + pwd_props = pwd_props & (~DOMAIN_PASSWORD_STORE_CLEARTEXT) + msgs.append("Plaintext password storage for changed passwords deactivated!") + + if complexity is not None or store_plaintext is not None: + m["pwdProperties"] = ldb.MessageElement(str(pwd_props), + ldb.FLAG_MOD_REPLACE, "pwdProperties") + + if history_length is not None: + if history_length == "default": + pwd_hist_len = 24 + else: + pwd_hist_len = int(history_length) + + if pwd_hist_len < 0 or pwd_hist_len > 24: + raise CommandError("Password history length must be in the range of 0 to 24!") + + m["pwdHistoryLength"] = ldb.MessageElement(str(pwd_hist_len), + ldb.FLAG_MOD_REPLACE, "pwdHistoryLength") + msgs.append("Password history length changed!") + + if min_pwd_length is not None: + if min_pwd_length == "default": + min_pwd_len = 7 + else: + min_pwd_len = int(min_pwd_length) + + if min_pwd_len < 0 or min_pwd_len > 14: + raise CommandError("Minimum password length must be in the range of 0 to 14!") + + m["minPwdLength"] = ldb.MessageElement(str(min_pwd_len), + ldb.FLAG_MOD_REPLACE, "minPwdLength") + msgs.append("Minimum password length changed!") + + if min_pwd_age is not None: + if min_pwd_age == "default": + min_pwd_age = 1 + else: + min_pwd_age = int(min_pwd_age) + + if min_pwd_age < 0 or min_pwd_age > 998: + raise CommandError("Minimum password age must be in the range of 0 to 998!") + + # days -> ticks + min_pwd_age_ticks = -int(min_pwd_age * (24 * 60 * 60 * 1e7)) + + m["minPwdAge"] = ldb.MessageElement(str(min_pwd_age_ticks), + ldb.FLAG_MOD_REPLACE, "minPwdAge") + msgs.append("Minimum password age changed!") + + if max_pwd_age is not None: + if max_pwd_age == "default": + max_pwd_age = 43 + else: + max_pwd_age = int(max_pwd_age) + + if max_pwd_age < 0 or max_pwd_age > 999: + raise CommandError("Maximum password age must be in the range of 0 to 999!") + + # days -> ticks + if max_pwd_age == 0: + max_pwd_age_ticks = -0x8000000000000000 + else: + max_pwd_age_ticks = -int(max_pwd_age * (24 * 60 * 60 * 1e7)) + + m["maxPwdAge"] = ldb.MessageElement(str(max_pwd_age_ticks), + ldb.FLAG_MOD_REPLACE, "maxPwdAge") + msgs.append("Maximum password age changed!") + + if max_pwd_age > 0 and min_pwd_age >= max_pwd_age: + raise CommandError("Maximum password age (%d) must be greater than minimum password age (%d)!" % (max_pwd_age, min_pwd_age)) + + if len(m) == 0: + raise CommandError("You must specify at least one option to set. Try --help") + samdb.modify(m) + msgs.append("All changes applied successfully!") + self.message("\n".join(msgs)) + else: + raise CommandError("Wrong argument '%s'!" % subcommand) + + +class cmd_domain_classicupgrade(Command): + """Upgrade from Samba classic (NT4-like) database to Samba AD DC database. + + Specify either a directory with all Samba classic DC databases and state files (with --dbdir) or + the testparm utility from your classic installation (with --testparm). + """ + + synopsis = "%prog [options] <classic_smb_conf>" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions + } + + takes_options = [ + Option("--dbdir", type="string", metavar="DIR", + help="Path to samba classic DC database directory"), + Option("--testparm", type="string", metavar="PATH", + help="Path to samba classic DC testparm utility from the previous installation. This allows the default paths of the previous installation to be followed"), + Option("--targetdir", type="string", metavar="DIR", + help="Path prefix where the new Samba 4.0 AD domain should be initialised"), + Option("--quiet", help="Be quiet", action="store_true"), + Option("--verbose", help="Be verbose", action="store_true"), + Option("--use-xattrs", type="choice", choices=["yes","no","auto"], metavar="[yes|no|auto]", + help="Define if we should use the native fs capabilities or a tdb file for storing attributes likes ntacl, auto tries to make an inteligent guess based on the user rights and system capabilities", default="auto"), + Option("--use-ntvfs", help="Use NTVFS for the fileserver (default = no)", + action="store_true"), + Option("--dns-backend", type="choice", metavar="NAMESERVER-BACKEND", + choices=["SAMBA_INTERNAL", "BIND9_FLATFILE", "BIND9_DLZ", "NONE"], + help="The DNS server backend. SAMBA_INTERNAL is the builtin name server (default), " + "BIND9_FLATFILE uses bind9 text database to store zone information, " + "BIND9_DLZ uses samba4 AD to store zone information, " + "NONE skips the DNS setup entirely (this DC will not be a DNS server)", + default="SAMBA_INTERNAL") + ] + + takes_args = ["smbconf"] + + def run(self, smbconf=None, targetdir=None, dbdir=None, testparm=None, + quiet=False, verbose=False, use_xattrs=None, sambaopts=None, versionopts=None, + dns_backend=None, use_ntvfs=False): + + if not os.path.exists(smbconf): + raise CommandError("File %s does not exist" % smbconf) + + if testparm and not os.path.exists(testparm): + raise CommandError("Testparm utility %s does not exist" % testparm) + + if dbdir and not os.path.exists(dbdir): + raise CommandError("Directory %s does not exist" % dbdir) + + if not dbdir and not testparm: + raise CommandError("Please specify either dbdir or testparm") + + logger = self.get_logger() + if verbose: + logger.setLevel(logging.DEBUG) + elif quiet: + logger.setLevel(logging.WARNING) + else: + logger.setLevel(logging.INFO) + + if dbdir and testparm: + logger.warning("both dbdir and testparm specified, ignoring dbdir.") + dbdir = None + + lp = sambaopts.get_loadparm() + + s3conf = s3param.get_context() + + if sambaopts.realm: + s3conf.set("realm", sambaopts.realm) + + if targetdir is not None: + if not os.path.isdir(targetdir): + os.mkdir(targetdir) + + eadb = True + if use_xattrs == "yes": + eadb = False + elif use_xattrs == "auto" and not s3conf.get("posix:eadb"): + if targetdir: + tmpfile = tempfile.NamedTemporaryFile(dir=os.path.abspath(targetdir)) + else: + tmpfile = tempfile.NamedTemporaryFile(dir=os.path.abspath(os.path.dirname(lp.get("private dir")))) + try: + try: + samba.ntacls.setntacl(lp, tmpfile.name, + "O:S-1-5-32G:S-1-5-32", "S-1-5-32", "native") + eadb = False + except Exception: + # FIXME: Don't catch all exceptions here + logger.info("You are not root or your system do not support xattr, using tdb backend for attributes. " + "If you intend to use this provision in production, rerun the script as root on a system supporting xattrs.") + finally: + tmpfile.close() + + # Set correct default values from dbdir or testparm + paths = {} + if dbdir: + paths["state directory"] = dbdir + paths["private dir"] = dbdir + paths["lock directory"] = dbdir + paths["smb passwd file"] = dbdir + "/smbpasswd" + else: + paths["state directory"] = get_testparm_var(testparm, smbconf, "state directory") + paths["private dir"] = get_testparm_var(testparm, smbconf, "private dir") + paths["smb passwd file"] = get_testparm_var(testparm, smbconf, "smb passwd file") + paths["lock directory"] = get_testparm_var(testparm, smbconf, "lock directory") + # "testparm" from Samba 3 < 3.4.x is not aware of the parameter + # "state directory", instead make use of "lock directory" + if len(paths["state directory"]) == 0: + paths["state directory"] = paths["lock directory"] + + for p in paths: + s3conf.set(p, paths[p]) + + # load smb.conf parameters + logger.info("Reading smb.conf") + s3conf.load(smbconf) + samba3 = Samba3(smbconf, s3conf) + + logger.info("Provisioning") + upgrade_from_samba3(samba3, logger, targetdir, session_info=system_session(), + useeadb=eadb, dns_backend=dns_backend, use_ntvfs=use_ntvfs) + + +class cmd_domain_samba3upgrade(cmd_domain_classicupgrade): + __doc__ = cmd_domain_classicupgrade.__doc__ + + # This command is present for backwards compatibility only, + # and should not be shown. + + hidden = True + + +class cmd_domain(SuperCommand): + """Domain management.""" + + subcommands = {} + subcommands["demote"] = cmd_domain_demote() + if cmd_domain_export_keytab is not None: + subcommands["exportkeytab"] = cmd_domain_export_keytab() + subcommands["info"] = cmd_domain_info() + subcommands["provision"] = cmd_domain_provision() + subcommands["join"] = cmd_domain_join() + subcommands["dcpromo"] = cmd_domain_dcpromo() + subcommands["level"] = cmd_domain_level() + subcommands["passwordsettings"] = cmd_domain_passwordsettings() + subcommands["classicupgrade"] = cmd_domain_classicupgrade() + subcommands["samba3upgrade"] = cmd_domain_samba3upgrade() diff --git a/python/samba/netcmd/drs.py b/python/samba/netcmd/drs.py new file mode 100644 index 00000000000..ff8d8304ebf --- /dev/null +++ b/python/samba/netcmd/drs.py @@ -0,0 +1,510 @@ +# implement samba_tool drs commands +# +# Copyright Andrew Tridgell 2010 +# +# based on C implementation by Kamen Mazdrashki <kamen.mazdrashki@postpath.com> +# +# 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 samba.getopt as options +import ldb + +from samba.auth import system_session +from samba.netcmd import ( + Command, + CommandError, + Option, + SuperCommand, + ) +from samba.samdb import SamDB +from samba import drs_utils, nttime2string, dsdb +from samba.dcerpc import drsuapi, misc +import common + +def drsuapi_connect(ctx): + '''make a DRSUAPI connection to the server''' + try: + (ctx.drsuapi, ctx.drsuapi_handle, ctx.bind_supported_extensions) = drs_utils.drsuapi_connect(ctx.server, ctx.lp, ctx.creds) + except Exception, e: + raise CommandError("DRS connection to %s failed" % ctx.server, e) + +def samdb_connect(ctx): + '''make a ldap connection to the server''' + try: + ctx.samdb = SamDB(url="ldap://%s" % ctx.server, + session_info=system_session(), + credentials=ctx.creds, lp=ctx.lp) + except Exception, e: + raise CommandError("LDAP connection to %s failed" % ctx.server, e) + +def drs_errmsg(werr): + '''return "was successful" or an error string''' + (ecode, estring) = werr + if ecode == 0: + return "was successful" + return "failed, result %u (%s)" % (ecode, estring) + + + +def attr_default(msg, attrname, default): + '''get an attribute from a ldap msg with a default''' + if attrname in msg: + return msg[attrname][0] + return default + + + +def drs_parse_ntds_dn(ntds_dn): + '''parse a NTDS DN returning a site and server''' + a = ntds_dn.split(',') + if a[0] != "CN=NTDS Settings" or a[2] != "CN=Servers" or a[4] != 'CN=Sites': + raise RuntimeError("bad NTDS DN %s" % ntds_dn) + server = a[1].split('=')[1] + site = a[3].split('=')[1] + return (site, server) + + + + + +class cmd_drs_showrepl(Command): + """Show replication status.""" + + synopsis = "%prog [<DC>] [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_args = ["DC?"] + + def print_neighbour(self, n): + '''print one set of neighbour information''' + self.message("%s" % n.naming_context_dn) + try: + (site, server) = drs_parse_ntds_dn(n.source_dsa_obj_dn) + self.message("\t%s\%s via RPC" % (site, server)) + except RuntimeError: + self.message("\tNTDS DN: %s" % n.source_dsa_obj_dn) + self.message("\t\tDSA object GUID: %s" % n.source_dsa_obj_guid) + self.message("\t\tLast attempt @ %s %s" % (nttime2string(n.last_attempt), + drs_errmsg(n.result_last_attempt))) + self.message("\t\t%u consecutive failure(s)." % n.consecutive_sync_failures) + self.message("\t\tLast success @ %s" % nttime2string(n.last_success)) + self.message("") + + def drsuapi_ReplicaInfo(ctx, info_type): + '''call a DsReplicaInfo''' + + req1 = drsuapi.DsReplicaGetInfoRequest1() + req1.info_type = info_type + try: + (info_type, info) = ctx.drsuapi.DsReplicaGetInfo(ctx.drsuapi_handle, 1, req1) + except Exception, e: + raise CommandError("DsReplicaGetInfo of type %u failed" % info_type, e) + return (info_type, info) + + def run(self, DC=None, sambaopts=None, + credopts=None, versionopts=None, server=None): + + self.lp = sambaopts.get_loadparm() + if DC is None: + DC = common.netcmd_dnsname(self.lp) + self.server = DC + self.creds = credopts.get_credentials(self.lp, fallback_machine=True) + + drsuapi_connect(self) + samdb_connect(self) + + # show domain information + ntds_dn = self.samdb.get_dsServiceName() + server_dns = self.samdb.search(base="", scope=ldb.SCOPE_BASE, attrs=["dnsHostName"])[0]['dnsHostName'][0] + + (site, server) = drs_parse_ntds_dn(ntds_dn) + try: + ntds = self.samdb.search(base=ntds_dn, scope=ldb.SCOPE_BASE, attrs=['options', 'objectGUID', 'invocationId']) + except Exception, e: + raise CommandError("Failed to search NTDS DN %s" % ntds_dn) + conn = self.samdb.search(base=ntds_dn, expression="(objectClass=nTDSConnection)") + + self.message("%s\\%s" % (site, server)) + self.message("DSA Options: 0x%08x" % int(attr_default(ntds[0], "options", 0))) + self.message("DSA object GUID: %s" % self.samdb.schema_format_value("objectGUID", ntds[0]["objectGUID"][0])) + self.message("DSA invocationId: %s\n" % self.samdb.schema_format_value("objectGUID", ntds[0]["invocationId"][0])) + + self.message("==== INBOUND NEIGHBORS ====\n") + (info_type, info) = self.drsuapi_ReplicaInfo(drsuapi.DRSUAPI_DS_REPLICA_INFO_NEIGHBORS) + for n in info.array: + self.print_neighbour(n) + + + self.message("==== OUTBOUND NEIGHBORS ====\n") + (info_type, info) = self.drsuapi_ReplicaInfo(drsuapi.DRSUAPI_DS_REPLICA_INFO_REPSTO) + for n in info.array: + self.print_neighbour(n) + + reasons = ['NTDSCONN_KCC_GC_TOPOLOGY', + 'NTDSCONN_KCC_RING_TOPOLOGY', + 'NTDSCONN_KCC_MINIMIZE_HOPS_TOPOLOGY', + 'NTDSCONN_KCC_STALE_SERVERS_TOPOLOGY', + 'NTDSCONN_KCC_OSCILLATING_CONNECTION_TOPOLOGY', + 'NTDSCONN_KCC_INTERSITE_GC_TOPOLOGY', + 'NTDSCONN_KCC_INTERSITE_TOPOLOGY', + 'NTDSCONN_KCC_SERVER_FAILOVER_TOPOLOGY', + 'NTDSCONN_KCC_SITE_FAILOVER_TOPOLOGY', + 'NTDSCONN_KCC_REDUNDANT_SERVER_TOPOLOGY'] + + self.message("==== KCC CONNECTION OBJECTS ====\n") + for c in conn: + c_rdn, sep, c_server_dn = c['fromServer'][0].partition(',') + c_server_res = self.samdb.search(base=c_server_dn, scope=ldb.SCOPE_BASE, attrs=["dnsHostName"]) + c_server_dns = c_server_res[0]["dnsHostName"][0] + self.message("Connection --") + self.message("\tConnection name: %s" % c['name'][0]) + self.message("\tEnabled : %s" % attr_default(c, 'enabledConnection', 'TRUE')) + self.message("\tServer DNS name : %s" % c_server_dns) + self.message("\tServer DN name : %s" % c['fromServer'][0]) + self.message("\t\tTransportType: RPC") + self.message("\t\toptions: 0x%08X" % int(attr_default(c, 'options', 0))) + if not 'mS-DS-ReplicatesNCReason' in c: + self.message("Warning: No NC replicated for Connection!") + continue + for r in c['mS-DS-ReplicatesNCReason']: + a = str(r).split(':') + self.message("\t\tReplicatesNC: %s" % a[3]) + self.message("\t\tReason: 0x%08x" % int(a[2])) + for s in reasons: + if getattr(dsdb, s, 0) & int(a[2]): + self.message("\t\t\t%s" % s) + + + +class cmd_drs_kcc(Command): + """Trigger knowledge consistency center run.""" + + synopsis = "%prog [<DC>] [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_args = ["DC?"] + + def run(self, DC=None, sambaopts=None, + credopts=None, versionopts=None, server=None): + + self.lp = sambaopts.get_loadparm() + if DC is None: + DC = common.netcmd_dnsname(self.lp) + self.server = DC + + self.creds = credopts.get_credentials(self.lp, fallback_machine=True) + + drsuapi_connect(self) + + req1 = drsuapi.DsExecuteKCC1() + try: + self.drsuapi.DsExecuteKCC(self.drsuapi_handle, 1, req1) + except Exception, e: + raise CommandError("DsExecuteKCC failed", e) + self.message("Consistency check on %s successful." % DC) + + + +def drs_local_replicate(self, SOURCE_DC, NC): + '''replicate from a source DC to the local SAM''' + + self.server = SOURCE_DC + drsuapi_connect(self) + + self.local_samdb = SamDB(session_info=system_session(), url=None, + credentials=self.creds, lp=self.lp) + + self.samdb = SamDB(url="ldap://%s" % self.server, + session_info=system_session(), + credentials=self.creds, lp=self.lp) + + # work out the source and destination GUIDs + res = self.local_samdb.search(base="", scope=ldb.SCOPE_BASE, attrs=["dsServiceName"]) + self.ntds_dn = res[0]["dsServiceName"][0] + + res = self.local_samdb.search(base=self.ntds_dn, scope=ldb.SCOPE_BASE, attrs=["objectGUID"]) + self.ntds_guid = misc.GUID(self.samdb.schema_format_value("objectGUID", res[0]["objectGUID"][0])) + + + source_dsa_invocation_id = misc.GUID(self.samdb.get_invocation_id()) + destination_dsa_guid = self.ntds_guid + + self.samdb.transaction_start() + repl = drs_utils.drs_Replicate("ncacn_ip_tcp:%s[seal]" % self.server, self.lp, + self.creds, self.local_samdb) + try: + repl.replicate(NC, source_dsa_invocation_id, destination_dsa_guid) + except Exception, e: + raise CommandError("Error replicating DN %s" % NC, e) + self.samdb.transaction_commit() + + + +class cmd_drs_replicate(Command): + """Replicate a naming context between two DCs.""" + + synopsis = "%prog <destinationDC> <sourceDC> <NC> [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_args = ["DEST_DC", "SOURCE_DC", "NC"] + + takes_options = [ + Option("--add-ref", help="use ADD_REF to add to repsTo on source", action="store_true"), + Option("--sync-forced", help="use SYNC_FORCED to force inbound replication", action="store_true"), + Option("--sync-all", help="use SYNC_ALL to replicate from all DCs", action="store_true"), + Option("--full-sync", help="resync all objects", action="store_true"), + Option("--local", help="pull changes directly into the local database (destination DC is ignored)", action="store_true"), + ] + + def run(self, DEST_DC, SOURCE_DC, NC, + add_ref=False, sync_forced=False, sync_all=False, full_sync=False, + local=False, sambaopts=None, credopts=None, versionopts=None, server=None): + + self.server = DEST_DC + self.lp = sambaopts.get_loadparm() + + self.creds = credopts.get_credentials(self.lp, fallback_machine=True) + + if local: + drs_local_replicate(self, SOURCE_DC, NC) + return + + drsuapi_connect(self) + samdb_connect(self) + + # we need to find the NTDS GUID of the source DC + msg = self.samdb.search(base=self.samdb.get_config_basedn(), + expression="(&(objectCategory=server)(|(name=%s)(dNSHostName=%s)))" % ( + ldb.binary_encode(SOURCE_DC), + ldb.binary_encode(SOURCE_DC)), + attrs=[]) + if len(msg) == 0: + raise CommandError("Failed to find source DC %s" % SOURCE_DC) + server_dn = msg[0]['dn'] + + msg = self.samdb.search(base=server_dn, scope=ldb.SCOPE_ONELEVEL, + expression="(|(objectCategory=nTDSDSA)(objectCategory=nTDSDSARO))", + attrs=['objectGUID', 'options']) + if len(msg) == 0: + raise CommandError("Failed to find source NTDS DN %s" % SOURCE_DC) + source_dsa_guid = msg[0]['objectGUID'][0] + dsa_options = int(attr_default(msg, 'options', 0)) + + + req_options = 0 + if not (dsa_options & dsdb.DS_NTDSDSA_OPT_DISABLE_OUTBOUND_REPL): + req_options |= drsuapi.DRSUAPI_DRS_WRIT_REP + if add_ref: + req_options |= drsuapi.DRSUAPI_DRS_ADD_REF + if sync_forced: + req_options |= drsuapi.DRSUAPI_DRS_SYNC_FORCED + if sync_all: + req_options |= drsuapi.DRSUAPI_DRS_SYNC_ALL + if full_sync: + req_options |= drsuapi.DRSUAPI_DRS_FULL_SYNC_NOW + + try: + drs_utils.sendDsReplicaSync(self.drsuapi, self.drsuapi_handle, source_dsa_guid, NC, req_options) + except drs_utils.drsException, estr: + raise CommandError("DsReplicaSync failed", estr) + self.message("Replicate from %s to %s was successful." % (SOURCE_DC, DEST_DC)) + + + +class cmd_drs_bind(Command): + """Show DRS capabilities of a server.""" + + synopsis = "%prog [<DC>] [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_args = ["DC?"] + + def run(self, DC=None, sambaopts=None, + credopts=None, versionopts=None, server=None): + + self.lp = sambaopts.get_loadparm() + if DC is None: + DC = common.netcmd_dnsname(self.lp) + self.server = DC + self.creds = credopts.get_credentials(self.lp, fallback_machine=True) + + drsuapi_connect(self) + + bind_info = drsuapi.DsBindInfoCtr() + bind_info.length = 28 + bind_info.info = drsuapi.DsBindInfo28() + (info, handle) = self.drsuapi.DsBind(misc.GUID(drsuapi.DRSUAPI_DS_BIND_GUID), bind_info) + + optmap = [ + ("DRSUAPI_SUPPORTED_EXTENSION_BASE", "DRS_EXT_BASE"), + ("DRSUAPI_SUPPORTED_EXTENSION_ASYNC_REPLICATION", "DRS_EXT_ASYNCREPL"), + ("DRSUAPI_SUPPORTED_EXTENSION_REMOVEAPI", "DRS_EXT_REMOVEAPI"), + ("DRSUAPI_SUPPORTED_EXTENSION_MOVEREQ_V2", "DRS_EXT_MOVEREQ_V2"), + ("DRSUAPI_SUPPORTED_EXTENSION_GETCHG_COMPRESS", "DRS_EXT_GETCHG_DEFLATE"), + ("DRSUAPI_SUPPORTED_EXTENSION_DCINFO_V1", "DRS_EXT_DCINFO_V1"), + ("DRSUAPI_SUPPORTED_EXTENSION_RESTORE_USN_OPTIMIZATION", "DRS_EXT_RESTORE_USN_OPTIMIZATION"), + ("DRSUAPI_SUPPORTED_EXTENSION_ADDENTRY", "DRS_EXT_ADDENTRY"), + ("DRSUAPI_SUPPORTED_EXTENSION_KCC_EXECUTE", "DRS_EXT_KCC_EXECUTE"), + ("DRSUAPI_SUPPORTED_EXTENSION_ADDENTRY_V2", "DRS_EXT_ADDENTRY_V2"), + ("DRSUAPI_SUPPORTED_EXTENSION_LINKED_VALUE_REPLICATION", "DRS_EXT_LINKED_VALUE_REPLICATION"), + ("DRSUAPI_SUPPORTED_EXTENSION_DCINFO_V2", "DRS_EXT_DCINFO_V2"), + ("DRSUAPI_SUPPORTED_EXTENSION_INSTANCE_TYPE_NOT_REQ_ON_MOD","DRS_EXT_INSTANCE_TYPE_NOT_REQ_ON_MOD"), + ("DRSUAPI_SUPPORTED_EXTENSION_CRYPTO_BIND", "DRS_EXT_CRYPTO_BIND"), + ("DRSUAPI_SUPPORTED_EXTENSION_GET_REPL_INFO", "DRS_EXT_GET_REPL_INFO"), + ("DRSUAPI_SUPPORTED_EXTENSION_STRONG_ENCRYPTION", "DRS_EXT_STRONG_ENCRYPTION"), + ("DRSUAPI_SUPPORTED_EXTENSION_DCINFO_V01", "DRS_EXT_DCINFO_VFFFFFFFF"), + ("DRSUAPI_SUPPORTED_EXTENSION_TRANSITIVE_MEMBERSHIP", "DRS_EXT_TRANSITIVE_MEMBERSHIP"), + ("DRSUAPI_SUPPORTED_EXTENSION_ADD_SID_HISTORY", "DRS_EXT_ADD_SID_HISTORY"), + ("DRSUAPI_SUPPORTED_EXTENSION_POST_BETA3", "DRS_EXT_POST_BETA3"), + ("DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V5", "DRS_EXT_GETCHGREQ_V5"), + ("DRSUAPI_SUPPORTED_EXTENSION_GET_MEMBERSHIPS2", "DRS_EXT_GETMEMBERSHIPS2"), + ("DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V6", "DRS_EXT_GETCHGREQ_V6"), + ("DRSUAPI_SUPPORTED_EXTENSION_NONDOMAIN_NCS", "DRS_EXT_NONDOMAIN_NCS"), + ("DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V8", "DRS_EXT_GETCHGREQ_V8"), + ("DRSUAPI_SUPPORTED_EXTENSION_GETCHGREPLY_V5", "DRS_EXT_GETCHGREPLY_V5"), + ("DRSUAPI_SUPPORTED_EXTENSION_GETCHGREPLY_V6", "DRS_EXT_GETCHGREPLY_V6"), + ("DRSUAPI_SUPPORTED_EXTENSION_ADDENTRYREPLY_V3", "DRS_EXT_WHISTLER_BETA3"), + ("DRSUAPI_SUPPORTED_EXTENSION_GETCHGREPLY_V7", "DRS_EXT_WHISTLER_BETA3"), + ("DRSUAPI_SUPPORTED_EXTENSION_VERIFY_OBJECT", "DRS_EXT_WHISTLER_BETA3"), + ("DRSUAPI_SUPPORTED_EXTENSION_XPRESS_COMPRESS", "DRS_EXT_W2K3_DEFLATE"), + ("DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V10", "DRS_EXT_GETCHGREQ_V10"), + ("DRSUAPI_SUPPORTED_EXTENSION_RESERVED_PART2", "DRS_EXT_RESERVED_FOR_WIN2K_OR_DOTNET_PART2"), + ("DRSUAPI_SUPPORTED_EXTENSION_RESERVED_PART3", "DRS_EXT_RESERVED_FOR_WIN2K_OR_DOTNET_PART3") + ] + + optmap_ext = [ + ("DRSUAPI_SUPPORTED_EXTENSION_ADAM", "DRS_EXT_ADAM"), + ("DRSUAPI_SUPPORTED_EXTENSION_LH_BETA2", "DRS_EXT_LH_BETA2"), + ("DRSUAPI_SUPPORTED_EXTENSION_RECYCLE_BIN", "DRS_EXT_RECYCLE_BIN")] + + self.message("Bind to %s succeeded." % DC) + self.message("Extensions supported:") + for (opt, str) in optmap: + optval = getattr(drsuapi, opt, 0) + if info.info.supported_extensions & optval: + yesno = "Yes" + else: + yesno = "No " + self.message(" %-60s: %s (%s)" % (opt, yesno, str)) + + if isinstance(info.info, drsuapi.DsBindInfo48): + self.message("\nExtended Extensions supported:") + for (opt, str) in optmap_ext: + optval = getattr(drsuapi, opt, 0) + if info.info.supported_extensions_ext & optval: + yesno = "Yes" + else: + yesno = "No " + self.message(" %-60s: %s (%s)" % (opt, yesno, str)) + + self.message("\nSite GUID: %s" % info.info.site_guid) + self.message("Repl epoch: %u" % info.info.repl_epoch) + if isinstance(info.info, drsuapi.DsBindInfo48): + self.message("Forest GUID: %s" % info.info.config_dn_guid) + + + +class cmd_drs_options(Command): + """Query or change 'options' for NTDS Settings object of a Domain Controller.""" + + synopsis = "%prog [<DC>] [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_args = ["DC?"] + + takes_options = [ + Option("--dsa-option", help="DSA option to enable/disable", type="str", + metavar="{+|-}IS_GC | {+|-}DISABLE_INBOUND_REPL | {+|-}DISABLE_OUTBOUND_REPL | {+|-}DISABLE_NTDSCONN_XLATE" ), + ] + + option_map = {"IS_GC": 0x00000001, + "DISABLE_INBOUND_REPL": 0x00000002, + "DISABLE_OUTBOUND_REPL": 0x00000004, + "DISABLE_NTDSCONN_XLATE": 0x00000008} + + def run(self, DC=None, dsa_option=None, + sambaopts=None, credopts=None, versionopts=None): + + self.lp = sambaopts.get_loadparm() + if DC is None: + DC = common.netcmd_dnsname(self.lp) + self.server = DC + self.creds = credopts.get_credentials(self.lp, fallback_machine=True) + + samdb_connect(self) + + ntds_dn = self.samdb.get_dsServiceName() + res = self.samdb.search(base=ntds_dn, scope=ldb.SCOPE_BASE, attrs=["options"]) + dsa_opts = int(res[0]["options"][0]) + + # print out current DSA options + cur_opts = [x for x in self.option_map if self.option_map[x] & dsa_opts] + self.message("Current DSA options: " + ", ".join(cur_opts)) + + # modify options + if dsa_option: + if dsa_option[:1] not in ("+", "-"): + raise CommandError("Unknown option %s" % dsa_option) + flag = dsa_option[1:] + if flag not in self.option_map.keys(): + raise CommandError("Unknown option %s" % dsa_option) + if dsa_option[:1] == "+": + dsa_opts |= self.option_map[flag] + else: + dsa_opts &= ~self.option_map[flag] + #save new options + m = ldb.Message() + m.dn = ldb.Dn(self.samdb, ntds_dn) + m["options"]= ldb.MessageElement(str(dsa_opts), ldb.FLAG_MOD_REPLACE, "options") + self.samdb.modify(m) + # print out new DSA options + cur_opts = [x for x in self.option_map if self.option_map[x] & dsa_opts] + self.message("New DSA options: " + ", ".join(cur_opts)) + + +class cmd_drs(SuperCommand): + """Directory Replication Services (DRS) management.""" + + subcommands = {} + subcommands["bind"] = cmd_drs_bind() + subcommands["kcc"] = cmd_drs_kcc() + subcommands["replicate"] = cmd_drs_replicate() + subcommands["showrepl"] = cmd_drs_showrepl() + subcommands["options"] = cmd_drs_options() diff --git a/python/samba/netcmd/dsacl.py b/python/samba/netcmd/dsacl.py new file mode 100644 index 00000000000..28aa843adbc --- /dev/null +++ b/python/samba/netcmd/dsacl.py @@ -0,0 +1,182 @@ +# Manipulate ACLs on directory objects +# +# Copyright (C) Nadezhda Ivanova <nivanova@samba.org> 2010 +# +# 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 samba.getopt as options +from samba.dcerpc import security +from samba.samdb import SamDB +from samba.ndr import ndr_unpack, ndr_pack +from samba.dcerpc.security import ( + GUID_DRS_ALLOCATE_RIDS, GUID_DRS_CHANGE_DOMAIN_MASTER, + GUID_DRS_CHANGE_INFR_MASTER, GUID_DRS_CHANGE_PDC, + GUID_DRS_CHANGE_RID_MASTER, GUID_DRS_CHANGE_SCHEMA_MASTER, + GUID_DRS_GET_CHANGES, GUID_DRS_GET_ALL_CHANGES, + GUID_DRS_GET_FILTERED_ATTRIBUTES, GUID_DRS_MANAGE_TOPOLOGY, + GUID_DRS_MONITOR_TOPOLOGY, GUID_DRS_REPL_SYNCRONIZE, + GUID_DRS_RO_REPL_SECRET_SYNC) + + +import ldb +from ldb import SCOPE_BASE +import re + +from samba.auth import system_session +from samba.netcmd import ( + Command, + CommandError, + SuperCommand, + Option, + ) + + + +class cmd_dsacl_set(Command): + """Modify access list on a directory object.""" + + synopsis = "%prog [options]" + car_help = """ The access control right to allow or deny """ + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", + type=str, metavar="URL", dest="H"), + Option("--car", type="choice", choices=["change-rid", + "change-pdc", + "change-infrastructure", + "change-schema", + "change-naming", + "allocate_rids", + "get-changes", + "get-changes-all", + "get-changes-filtered", + "topology-manage", + "topology-monitor", + "repl-sync", + "ro-repl-secret-sync"], + help=car_help), + Option("--action", type="choice", choices=["allow", "deny"], + help="""Deny or allow access"""), + Option("--objectdn", help="DN of the object whose SD to modify", + type="string"), + Option("--trusteedn", help="DN of the entity that gets access", + type="string"), + Option("--sddl", help="An ACE or group of ACEs to be added on the object", + type="string"), + ] + + def find_trustee_sid(self, samdb, trusteedn): + res = samdb.search(base=trusteedn, expression="(objectClass=*)", + scope=SCOPE_BASE) + assert(len(res) == 1) + return ndr_unpack( security.dom_sid,res[0]["objectSid"][0]) + + def modify_descriptor(self, samdb, object_dn, desc, controls=None): + assert(isinstance(desc, security.descriptor)) + m = ldb.Message() + m.dn = ldb.Dn(samdb, object_dn) + m["nTSecurityDescriptor"]= ldb.MessageElement( + (ndr_pack(desc)), ldb.FLAG_MOD_REPLACE, + "nTSecurityDescriptor") + samdb.modify(m) + + def read_descriptor(self, samdb, object_dn): + res = samdb.search(base=object_dn, scope=SCOPE_BASE, + attrs=["nTSecurityDescriptor"]) + # we should theoretically always have an SD + assert(len(res) == 1) + desc = res[0]["nTSecurityDescriptor"][0] + return ndr_unpack(security.descriptor, desc) + + def get_domain_sid(self, samdb): + res = samdb.search(base=samdb.domain_dn(), + expression="(objectClass=*)", scope=SCOPE_BASE) + return ndr_unpack( security.dom_sid,res[0]["objectSid"][0]) + + def add_ace(self, samdb, object_dn, new_ace): + """Add new ace explicitly.""" + desc = self.read_descriptor(samdb, object_dn) + desc_sddl = desc.as_sddl(self.get_domain_sid(samdb)) + #TODO add bindings for descriptor manipulation and get rid of this + desc_aces = re.findall("\(.*?\)", desc_sddl) + for ace in desc_aces: + if ("ID" in ace): + desc_sddl = desc_sddl.replace(ace, "") + if new_ace in desc_sddl: + return + if desc_sddl.find("(") >= 0: + desc_sddl = desc_sddl[:desc_sddl.index("(")] + new_ace + desc_sddl[desc_sddl.index("("):] + else: + desc_sddl = desc_sddl + new_ace + desc = security.descriptor.from_sddl(desc_sddl, self.get_domain_sid(samdb)) + self.modify_descriptor(samdb, object_dn, desc) + + def print_new_acl(self, samdb, object_dn): + desc = self.read_descriptor(samdb, object_dn) + desc_sddl = desc.as_sddl(self.get_domain_sid(samdb)) + self.outf.write("new descriptor for %s:\n" % object_dn) + self.outf.write(desc_sddl + "\n") + + def run(self, car, action, objectdn, trusteedn, sddl, + H=None, credopts=None, sambaopts=None, versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + + if sddl is None and (car is None or action is None + or objectdn is None or trusteedn is None): + return self.usage() + + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + cars = {'change-rid' : GUID_DRS_CHANGE_RID_MASTER, + 'change-pdc' : GUID_DRS_CHANGE_PDC, + 'change-infrastructure' : GUID_DRS_CHANGE_INFR_MASTER, + 'change-schema' : GUID_DRS_CHANGE_SCHEMA_MASTER, + 'change-naming' : GUID_DRS_CHANGE_DOMAIN_MASTER, + 'allocate_rids' : GUID_DRS_ALLOCATE_RIDS, + 'get-changes' : GUID_DRS_GET_CHANGES, + 'get-changes-all' : GUID_DRS_GET_ALL_CHANGES, + 'get-changes-filtered' : GUID_DRS_GET_FILTERED_ATTRIBUTES, + 'topology-manage' : GUID_DRS_MANAGE_TOPOLOGY, + 'topology-monitor' : GUID_DRS_MONITOR_TOPOLOGY, + 'repl-sync' : GUID_DRS_REPL_SYNCRONIZE, + 'ro-repl-secret-sync' : GUID_DRS_RO_REPL_SECRET_SYNC, + } + sid = self.find_trustee_sid(samdb, trusteedn) + if sddl: + new_ace = sddl + elif action == "allow": + new_ace = "(OA;;CR;%s;;%s)" % (cars[car], str(sid)) + elif action == "deny": + new_ace = "(OD;;CR;%s;;%s)" % (cars[car], str(sid)) + else: + raise CommandError("Wrong argument '%s'!" % action) + + self.print_new_acl(samdb, objectdn) + self.add_ace(samdb, objectdn, new_ace) + self.print_new_acl(samdb, objectdn) + + +class cmd_dsacl(SuperCommand): + """DS ACLs manipulation.""" + + subcommands = {} + subcommands["set"] = cmd_dsacl_set() diff --git a/python/samba/netcmd/fsmo.py b/python/samba/netcmd/fsmo.py new file mode 100644 index 00000000000..c938c915fa9 --- /dev/null +++ b/python/samba/netcmd/fsmo.py @@ -0,0 +1,277 @@ +# Changes a FSMO role owner +# +# Copyright Nadezhda Ivanova 2009 +# Copyright Jelmer Vernooij 2009 +# +# 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 samba.getopt as options +import ldb +from ldb import LdbError + +from samba.auth import system_session +from samba.netcmd import ( + Command, + CommandError, + SuperCommand, + Option, + ) +from samba.samdb import SamDB + +def transfer_role(outf, role, samdb): + m = ldb.Message() + m.dn = ldb.Dn(samdb, "") + if role == "rid": + m["becomeRidMaster"]= ldb.MessageElement( + "1", ldb.FLAG_MOD_REPLACE, + "becomeRidMaster") + elif role == "pdc": + domain_dn = samdb.domain_dn() + res = samdb.search(domain_dn, + scope=ldb.SCOPE_BASE, attrs=["objectSid"]) + assert len(res) == 1 + sid = res[0]["objectSid"][0] + m["becomePdc"]= ldb.MessageElement( + sid, ldb.FLAG_MOD_REPLACE, + "becomePdc") + elif role == "naming": + m["becomeDomainMaster"]= ldb.MessageElement( + "1", ldb.FLAG_MOD_REPLACE, + "becomeDomainMaster") + samdb.modify(m) + elif role == "infrastructure": + m["becomeInfrastructureMaster"]= ldb.MessageElement( + "1", ldb.FLAG_MOD_REPLACE, + "becomeInfrastructureMaster") + elif role == "schema": + m["becomeSchemaMaster"]= ldb.MessageElement( + "1", ldb.FLAG_MOD_REPLACE, + "becomeSchemaMaster") + else: + raise CommandError("Invalid FSMO role.") + try: + samdb.modify(m) + except LdbError, (num, msg): + raise CommandError("Failed to initiate transfer of '%s' role: %s" % (role, msg)) + outf.write("FSMO transfer of '%s' role successful\n" % role) + + +class cmd_fsmo_seize(Command): + """Seize the role.""" + + synopsis = "%prog [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H"), + Option("--force", help="Force seizing of the role without attempting to transfer first.", action="store_true"), + Option("--role", type="choice", choices=["rid", "pdc", "infrastructure","schema","naming","all"], + help="""The FSMO role to seize or transfer.\n +rid=RidAllocationMasterRole\n +schema=SchemaMasterRole\n +pdc=PdcEmulationMasterRole\n +naming=DomainNamingMasterRole\n +infrastructure=InfrastructureMasterRole\n +all=all of the above"""), + ] + + takes_args = [] + + def seize_role(self, role, samdb, force): + res = samdb.search("", + scope=ldb.SCOPE_BASE, attrs=["dsServiceName"]) + assert len(res) == 1 + serviceName = res[0]["dsServiceName"][0] + domain_dn = samdb.domain_dn() + self.infrastructure_dn = "CN=Infrastructure," + domain_dn + self.naming_dn = "CN=Partitions,%s" % samdb.get_config_basedn() + self.schema_dn = str(samdb.get_schema_basedn()) + self.rid_dn = "CN=RID Manager$,CN=System," + domain_dn + + m = ldb.Message() + if role == "rid": + m.dn = ldb.Dn(samdb, self.rid_dn) + elif role == "pdc": + m.dn = ldb.Dn(samdb, domain_dn) + elif role == "naming": + m.dn = ldb.Dn(samdb, self.naming_dn) + elif role == "infrastructure": + m.dn = ldb.Dn(samdb, self.infrastructure_dn) + elif role == "schema": + m.dn = ldb.Dn(samdb, self.schema_dn) + else: + raise CommandError("Invalid FSMO role.") + #first try to transfer to avoid problem if the owner is still active + if force is None: + self.message("Attempting transfer...") + try: + transfer_role(self.outf, role, samdb) + except CommandError: + #transfer failed, use the big axe... + self.message("Transfer unsuccessful, seizing...") + m["fSMORoleOwner"]= ldb.MessageElement( + serviceName, ldb.FLAG_MOD_REPLACE, + "fSMORoleOwner") + else: + self.message("Will not attempt transfer, seizing...") + m["fSMORoleOwner"]= ldb.MessageElement( + serviceName, ldb.FLAG_MOD_REPLACE, + "fSMORoleOwner") + try: + samdb.modify(m) + except LdbError, (num, msg): + raise CommandError("Failed to initiate role seize of '%s' role: %s" % (role, msg)) + self.outf.write("FSMO transfer of '%s' role successful\n" % role) + + def run(self, force=None, H=None, role=None, + credopts=None, sambaopts=None, versionopts=None): + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + + if role == "all": + self.seize_role("rid", samdb, force) + self.seize_role("pdc", samdb, force) + self.seize_role("naming", samdb, force) + self.seize_role("infrastructure", samdb, force) + self.seize_role("schema", samdb, force) + else: + self.seize_role(role, samdb, force) + + +class cmd_fsmo_show(Command): + """Show the roles.""" + + synopsis = "%prog [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H"), + ] + + takes_args = [] + + def run(self, H=None, credopts=None, sambaopts=None, versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + + domain_dn = samdb.domain_dn() + self.infrastructure_dn = "CN=Infrastructure," + domain_dn + self.naming_dn = "CN=Partitions,%s" % samdb.get_config_basedn() + self.schema_dn = samdb.get_schema_basedn() + self.rid_dn = "CN=RID Manager$,CN=System," + domain_dn + + res = samdb.search(self.infrastructure_dn, + scope=ldb.SCOPE_BASE, attrs=["fSMORoleOwner"]) + assert len(res) == 1 + self.infrastructureMaster = res[0]["fSMORoleOwner"][0] + + res = samdb.search(domain_dn, + scope=ldb.SCOPE_BASE, attrs=["fSMORoleOwner"]) + assert len(res) == 1 + self.pdcEmulator = res[0]["fSMORoleOwner"][0] + + res = samdb.search(self.naming_dn, + scope=ldb.SCOPE_BASE, attrs=["fSMORoleOwner"]) + assert len(res) == 1 + self.namingMaster = res[0]["fSMORoleOwner"][0] + + res = samdb.search(self.schema_dn, + scope=ldb.SCOPE_BASE, attrs=["fSMORoleOwner"]) + assert len(res) == 1 + self.schemaMaster = res[0]["fSMORoleOwner"][0] + + res = samdb.search(self.rid_dn, + scope=ldb.SCOPE_BASE, attrs=["fSMORoleOwner"]) + assert len(res) == 1 + self.ridMaster = res[0]["fSMORoleOwner"][0] + + self.message("InfrastructureMasterRole owner: " + self.infrastructureMaster) + self.message("RidAllocationMasterRole owner: " + self.ridMaster) + self.message("PdcEmulationMasterRole owner: " + self.pdcEmulator) + self.message("DomainNamingMasterRole owner: " + self.namingMaster) + self.message("SchemaMasterRole owner: " + self.schemaMaster) + + +class cmd_fsmo_transfer(Command): + """Transfer the role.""" + + synopsis = "%prog [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H"), + Option("--role", type="choice", choices=["rid", "pdc", "infrastructure","schema","naming","all"], + help="""The FSMO role to seize or transfer.\n +rid=RidAllocationMasterRole\n +schema=SchemaMasterRole\n +pdc=PdcEmulationMasterRole\n +naming=DomainNamingMasterRole\n +infrastructure=InfrastructureMasterRole\n +all=all of the above"""), + ] + + takes_args = [] + + def run(self, force=None, H=None, role=None, + credopts=None, sambaopts=None, versionopts=None): + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + + if role == "all": + transfer_role(self.outf, "rid", samdb) + transfer_role(self.outf, "pdc", samdb) + transfer_role(self.outf, "naming", samdb) + transfer_role(self.outf, "infrastructure", samdb) + transfer_role(self.outf, "schema", samdb) + else: + transfer_role(self.outf, role, samdb) + + +class cmd_fsmo(SuperCommand): + """Flexible Single Master Operations (FSMO) roles management.""" + + subcommands = {} + subcommands["seize"] = cmd_fsmo_seize() + subcommands["show"] = cmd_fsmo_show() + subcommands["transfer"] = cmd_fsmo_transfer() diff --git a/python/samba/netcmd/gpo.py b/python/samba/netcmd/gpo.py new file mode 100644 index 00000000000..23b562eb635 --- /dev/null +++ b/python/samba/netcmd/gpo.py @@ -0,0 +1,1177 @@ +# implement samba_tool gpo commands +# +# Copyright Andrew Tridgell 2010 +# Copyright Amitay Isaacs 2011-2012 <amitay@gmail.com> +# +# based on C implementation by Guenther Deschner and Wilco Baan Hofman +# +# 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 os +import samba.getopt as options +import ldb + +from samba.auth import system_session +from samba.netcmd import ( + Command, + CommandError, + Option, + SuperCommand, + ) +from samba.samdb import SamDB +from samba import dsdb +from samba.dcerpc import security +from samba.ndr import ndr_unpack +import samba.security +import samba.auth +from samba.auth import AUTH_SESSION_INFO_DEFAULT_GROUPS, AUTH_SESSION_INFO_AUTHENTICATED, AUTH_SESSION_INFO_SIMPLE_PRIVILEGES +from samba.netcmd.common import netcmd_finddc +from samba import policy +from samba import smb +import uuid +from samba.ntacls import dsacl2fsacl +from samba.dcerpc import nbt +from samba.net import Net + + +def samdb_connect(ctx): + '''make a ldap connection to the server''' + try: + ctx.samdb = SamDB(url=ctx.url, + session_info=system_session(), + credentials=ctx.creds, lp=ctx.lp) + except Exception, e: + raise CommandError("LDAP connection to %s failed " % ctx.url, e) + + +def attr_default(msg, attrname, default): + '''get an attribute from a ldap msg with a default''' + if attrname in msg: + return msg[attrname][0] + return default + + +def gpo_flags_string(value): + '''return gpo flags string''' + flags = policy.get_gpo_flags(value) + if not flags: + ret = 'NONE' + else: + ret = ' '.join(flags) + return ret + + +def gplink_options_string(value): + '''return gplink options string''' + options = policy.get_gplink_options(value) + if not options: + ret = 'NONE' + else: + ret = ' '.join(options) + return ret + + +def parse_gplink(gplink): + '''parse a gPLink into an array of dn and options''' + ret = [] + a = gplink.split(']') + for g in a: + if not g: + continue + d = g.split(';') + if len(d) != 2 or not d[0].startswith("[LDAP://"): + raise RuntimeError("Badly formed gPLink '%s'" % g) + ret.append({ 'dn' : d[0][8:], 'options' : int(d[1])}) + return ret + + +def encode_gplink(gplist): + '''Encode an array of dn and options into gPLink string''' + ret = '' + for g in gplist: + ret += "[LDAP://%s;%d]" % (g['dn'], g['options']) + return ret + + +def dc_url(lp, creds, url=None, dc=None): + '''If URL is not specified, return URL for writable DC. + If dc is provided, use that to construct ldap URL''' + + if url is None: + if dc is None: + try: + dc = netcmd_finddc(lp, creds) + except Exception, e: + raise RuntimeError("Could not find a DC for domain", e) + url = 'ldap://' + dc + return url + + +def get_gpo_dn(samdb, gpo): + '''Construct the DN for gpo''' + + dn = samdb.get_default_basedn() + dn.add_child(ldb.Dn(samdb, "CN=Policies,CN=System")) + dn.add_child(ldb.Dn(samdb, "CN=%s" % gpo)) + return dn + + +def get_gpo_info(samdb, gpo=None, displayname=None, dn=None, + sd_flags=security.SECINFO_OWNER|security.SECINFO_GROUP|security.SECINFO_DACL|security.SECINFO_SACL): + '''Get GPO information using gpo, displayname or dn''' + + policies_dn = samdb.get_default_basedn() + policies_dn.add_child(ldb.Dn(samdb, "CN=Policies,CN=System")) + + base_dn = policies_dn + search_expr = "(objectClass=groupPolicyContainer)" + search_scope = ldb.SCOPE_ONELEVEL + + if gpo is not None: + search_expr = "(&(objectClass=groupPolicyContainer)(name=%s))" % ldb.binary_encode(gpo) + + if displayname is not None: + search_expr = "(&(objectClass=groupPolicyContainer)(displayname=%s))" % ldb.binary_encode(displayname) + + if dn is not None: + base_dn = dn + search_scope = ldb.SCOPE_BASE + + try: + msg = samdb.search(base=base_dn, scope=search_scope, + expression=search_expr, + attrs=['nTSecurityDescriptor', + 'versionNumber', + 'flags', + 'name', + 'displayName', + 'gPCFileSysPath'], + controls=['sd_flags:1:%d' % sd_flags]) + except Exception, e: + if gpo is not None: + mesg = "Cannot get information for GPO %s" % gpo + else: + mesg = "Cannot get information for GPOs" + raise CommandError(mesg, e) + + return msg + + +def get_gpo_containers(samdb, gpo): + '''lists dn of containers for a GPO''' + + search_expr = "(&(objectClass=*)(gPLink=*%s*))" % gpo + try: + msg = samdb.search(expression=search_expr, attrs=['gPLink']) + except Exception, e: + raise CommandError("Could not find container(s) with GPO %s" % gpo, e) + + return msg + + +def del_gpo_link(samdb, container_dn, gpo): + '''delete GPO link for the container''' + # Check if valid Container DN and get existing GPlinks + try: + msg = samdb.search(base=container_dn, scope=ldb.SCOPE_BASE, + expression="(objectClass=*)", + attrs=['gPLink'])[0] + except Exception, e: + raise CommandError("Container '%s' does not exist" % container_dn, e) + + found = False + gpo_dn = str(get_gpo_dn(samdb, gpo)) + if 'gPLink' in msg: + gplist = parse_gplink(msg['gPLink'][0]) + for g in gplist: + if g['dn'].lower() == gpo_dn.lower(): + gplist.remove(g) + found = True + break + else: + raise CommandError("No GPO(s) linked to this container") + + if not found: + raise CommandError("GPO '%s' not linked to this container" % gpo) + + m = ldb.Message() + m.dn = container_dn + if gplist: + gplink_str = encode_gplink(gplist) + m['r0'] = ldb.MessageElement(gplink_str, ldb.FLAG_MOD_REPLACE, 'gPLink') + else: + m['d0'] = ldb.MessageElement(msg['gPLink'][0], ldb.FLAG_MOD_DELETE, 'gPLink') + try: + samdb.modify(m) + except Exception, e: + raise CommandError("Error removing GPO from container", e) + + +def parse_unc(unc): + '''Parse UNC string into a hostname, a service, and a filepath''' + if unc.startswith('\\\\') and unc.startswith('//'): + raise ValueError("UNC doesn't start with \\\\ or //") + tmp = unc[2:].split('/', 2) + if len(tmp) == 3: + return tmp + tmp = unc[2:].split('\\', 2) + if len(tmp) == 3: + return tmp + raise ValueError("Invalid UNC string: %s" % unc) + + +def copy_directory_remote_to_local(conn, remotedir, localdir): + if not os.path.isdir(localdir): + os.mkdir(localdir) + r_dirs = [ remotedir ] + l_dirs = [ localdir ] + while r_dirs: + r_dir = r_dirs.pop() + l_dir = l_dirs.pop() + + dirlist = conn.list(r_dir) + for e in dirlist: + r_name = r_dir + '\\' + e['name'] + l_name = os.path.join(l_dir, e['name']) + + if e['attrib'] & smb.FILE_ATTRIBUTE_DIRECTORY: + r_dirs.append(r_name) + l_dirs.append(l_name) + os.mkdir(l_name) + else: + data = conn.loadfile(r_name) + file(l_name, 'w').write(data) + + +def copy_directory_local_to_remote(conn, localdir, remotedir): + if not conn.chkpath(remotedir): + conn.mkdir(remotedir) + l_dirs = [ localdir ] + r_dirs = [ remotedir ] + while l_dirs: + l_dir = l_dirs.pop() + r_dir = r_dirs.pop() + + dirlist = os.listdir(l_dir) + for e in dirlist: + l_name = os.path.join(l_dir, e) + r_name = r_dir + '\\' + e + + if os.path.isdir(l_name): + l_dirs.append(l_name) + r_dirs.append(r_name) + conn.mkdir(r_name) + else: + data = file(l_name, 'r').read() + conn.savefile(r_name, data) + + +def create_directory_hier(conn, remotedir): + elems = remotedir.replace('/', '\\').split('\\') + path = "" + for e in elems: + path = path + '\\' + e + if not conn.chkpath(path): + conn.mkdir(path) + + +class cmd_listall(Command): + """List all GPOs.""" + + synopsis = "%prog [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H") + ] + + def run(self, H=None, sambaopts=None, credopts=None, versionopts=None): + + self.lp = sambaopts.get_loadparm() + self.creds = credopts.get_credentials(self.lp, fallback_machine=True) + + self.url = dc_url(self.lp, self.creds, H) + + samdb_connect(self) + + msg = get_gpo_info(self.samdb, None) + + for m in msg: + self.outf.write("GPO : %s\n" % m['name'][0]) + self.outf.write("display name : %s\n" % m['displayName'][0]) + self.outf.write("path : %s\n" % m['gPCFileSysPath'][0]) + self.outf.write("dn : %s\n" % m.dn) + self.outf.write("version : %s\n" % attr_default(m, 'versionNumber', '0')) + self.outf.write("flags : %s\n" % gpo_flags_string(int(attr_default(m, 'flags', 0)))) + self.outf.write("\n") + + +class cmd_list(Command): + """List GPOs for an account.""" + + synopsis = "%prog <username> [options]" + + takes_args = ['username'] + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", + type=str, metavar="URL", dest="H") + ] + + def run(self, username, H=None, sambaopts=None, credopts=None, versionopts=None): + + self.lp = sambaopts.get_loadparm() + self.creds = credopts.get_credentials(self.lp, fallback_machine=True) + + self.url = dc_url(self.lp, self.creds, H) + + samdb_connect(self) + + try: + msg = self.samdb.search(expression='(&(|(samAccountName=%s)(samAccountName=%s$))(objectClass=User))' % + (ldb.binary_encode(username),ldb.binary_encode(username))) + user_dn = msg[0].dn + except Exception: + raise CommandError("Failed to find account %s" % username) + + # check if its a computer account + try: + msg = self.samdb.search(base=user_dn, scope=ldb.SCOPE_BASE, attrs=['objectClass'])[0] + is_computer = 'computer' in msg['objectClass'] + except Exception: + raise CommandError("Failed to find objectClass for user %s" % username) + + session_info_flags = ( AUTH_SESSION_INFO_DEFAULT_GROUPS | + AUTH_SESSION_INFO_AUTHENTICATED ) + + # When connecting to a remote server, don't look up the local privilege DB + if self.url is not None and self.url.startswith('ldap'): + session_info_flags |= AUTH_SESSION_INFO_SIMPLE_PRIVILEGES + + session = samba.auth.user_session(self.samdb, lp_ctx=self.lp, dn=user_dn, + session_info_flags=session_info_flags) + + token = session.security_token + + gpos = [] + + inherit = True + dn = ldb.Dn(self.samdb, str(user_dn)).parent() + while True: + msg = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE, attrs=['gPLink', 'gPOptions'])[0] + if 'gPLink' in msg: + glist = parse_gplink(msg['gPLink'][0]) + for g in glist: + if not inherit and not (g['options'] & dsdb.GPLINK_OPT_ENFORCE): + continue + if g['options'] & dsdb.GPLINK_OPT_DISABLE: + continue + + try: + sd_flags=security.SECINFO_OWNER|security.SECINFO_GROUP|security.SECINFO_DACL + gmsg = self.samdb.search(base=g['dn'], scope=ldb.SCOPE_BASE, + attrs=['name', 'displayName', 'flags', + 'nTSecurityDescriptor'], + controls=['sd_flags:1:%d' % sd_flags]) + secdesc_ndr = gmsg[0]['nTSecurityDescriptor'][0] + secdesc = ndr_unpack(security.descriptor, secdesc_ndr) + except Exception: + self.outf.write("Failed to fetch gpo object with nTSecurityDescriptor %s\n" % + g['dn']) + continue + + try: + samba.security.access_check(secdesc, token, + security.SEC_STD_READ_CONTROL | + security.SEC_ADS_LIST | + security.SEC_ADS_READ_PROP) + except RuntimeError: + self.outf.write("Failed access check on %s\n" % msg.dn) + continue + + # check the flags on the GPO + flags = int(attr_default(gmsg[0], 'flags', 0)) + if is_computer and (flags & dsdb.GPO_FLAG_MACHINE_DISABLE): + continue + if not is_computer and (flags & dsdb.GPO_FLAG_USER_DISABLE): + continue + gpos.append((gmsg[0]['displayName'][0], gmsg[0]['name'][0])) + + # check if this blocks inheritance + gpoptions = int(attr_default(msg, 'gPOptions', 0)) + if gpoptions & dsdb.GPO_BLOCK_INHERITANCE: + inherit = False + + if dn == self.samdb.get_default_basedn(): + break + dn = dn.parent() + + if is_computer: + msg_str = 'computer' + else: + msg_str = 'user' + + self.outf.write("GPOs for %s %s\n" % (msg_str, username)) + for g in gpos: + self.outf.write(" %s %s\n" % (g[0], g[1])) + + +class cmd_show(Command): + """Show information for a GPO.""" + + synopsis = "%prog <gpo> [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_args = ['gpo'] + + takes_options = [ + Option("-H", help="LDB URL for database or target server", type=str) + ] + + def run(self, gpo, H=None, sambaopts=None, credopts=None, versionopts=None): + + self.lp = sambaopts.get_loadparm() + self.creds = credopts.get_credentials(self.lp, fallback_machine=True) + + self.url = dc_url(self.lp, self.creds, H) + + samdb_connect(self) + + try: + msg = get_gpo_info(self.samdb, gpo)[0] + except Exception: + raise CommandError("GPO '%s' does not exist" % gpo) + + try: + secdesc_ndr = msg['nTSecurityDescriptor'][0] + secdesc = ndr_unpack(security.descriptor, secdesc_ndr) + secdesc_sddl = secdesc.as_sddl() + except Exception: + secdesc_sddl = "<hidden>" + + self.outf.write("GPO : %s\n" % msg['name'][0]) + self.outf.write("display name : %s\n" % msg['displayName'][0]) + self.outf.write("path : %s\n" % msg['gPCFileSysPath'][0]) + self.outf.write("dn : %s\n" % msg.dn) + self.outf.write("version : %s\n" % attr_default(msg, 'versionNumber', '0')) + self.outf.write("flags : %s\n" % gpo_flags_string(int(attr_default(msg, 'flags', 0)))) + self.outf.write("ACL : %s\n" % secdesc_sddl) + self.outf.write("\n") + + +class cmd_getlink(Command): + """List GPO Links for a container.""" + + synopsis = "%prog <container_dn> [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_args = ['container_dn'] + + takes_options = [ + Option("-H", help="LDB URL for database or target server", type=str) + ] + + def run(self, container_dn, H=None, sambaopts=None, credopts=None, + versionopts=None): + + self.lp = sambaopts.get_loadparm() + self.creds = credopts.get_credentials(self.lp, fallback_machine=True) + + self.url = dc_url(self.lp, self.creds, H) + + samdb_connect(self) + + try: + msg = self.samdb.search(base=container_dn, scope=ldb.SCOPE_BASE, + expression="(objectClass=*)", + attrs=['gPLink'])[0] + except Exception: + raise CommandError("Container '%s' does not exist" % container_dn) + + if msg['gPLink']: + self.outf.write("GPO(s) linked to DN %s\n" % container_dn) + gplist = parse_gplink(msg['gPLink'][0]) + for g in gplist: + msg = get_gpo_info(self.samdb, dn=g['dn']) + self.outf.write(" GPO : %s\n" % msg[0]['name'][0]) + self.outf.write(" Name : %s\n" % msg[0]['displayName'][0]) + self.outf.write(" Options : %s\n" % gplink_options_string(g['options'])) + self.outf.write("\n") + else: + self.outf.write("No GPO(s) linked to DN=%s\n" % container_dn) + + +class cmd_setlink(Command): + """Add or update a GPO link to a container.""" + + synopsis = "%prog <container_dn> <gpo> [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_args = ['container_dn', 'gpo'] + + takes_options = [ + Option("-H", help="LDB URL for database or target server", type=str), + Option("--disable", dest="disabled", default=False, action='store_true', + help="Disable policy"), + Option("--enforce", dest="enforced", default=False, action='store_true', + help="Enforce policy") + ] + + def run(self, container_dn, gpo, H=None, disabled=False, enforced=False, + sambaopts=None, credopts=None, versionopts=None): + + self.lp = sambaopts.get_loadparm() + self.creds = credopts.get_credentials(self.lp, fallback_machine=True) + + self.url = dc_url(self.lp, self.creds, H) + + samdb_connect(self) + + gplink_options = 0 + if disabled: + gplink_options |= dsdb.GPLINK_OPT_DISABLE + if enforced: + gplink_options |= dsdb.GPLINK_OPT_ENFORCE + + # Check if valid GPO DN + try: + msg = get_gpo_info(self.samdb, gpo=gpo)[0] + except Exception: + raise CommandError("GPO '%s' does not exist" % gpo) + gpo_dn = str(get_gpo_dn(self.samdb, gpo)) + + # Check if valid Container DN + try: + msg = self.samdb.search(base=container_dn, scope=ldb.SCOPE_BASE, + expression="(objectClass=*)", + attrs=['gPLink'])[0] + except Exception: + raise CommandError("Container '%s' does not exist" % container_dn) + + # Update existing GPlinks or Add new one + existing_gplink = False + if 'gPLink' in msg: + gplist = parse_gplink(msg['gPLink'][0]) + existing_gplink = True + found = False + for g in gplist: + if g['dn'].lower() == gpo_dn.lower(): + g['options'] = gplink_options + found = True + break + if found: + raise CommandError("GPO '%s' already linked to this container" % gpo) + else: + gplist.insert(0, { 'dn' : gpo_dn, 'options' : gplink_options }) + else: + gplist = [] + gplist.append({ 'dn' : gpo_dn, 'options' : gplink_options }) + + gplink_str = encode_gplink(gplist) + + m = ldb.Message() + m.dn = ldb.Dn(self.samdb, container_dn) + + if existing_gplink: + m['new_value'] = ldb.MessageElement(gplink_str, ldb.FLAG_MOD_REPLACE, 'gPLink') + else: + m['new_value'] = ldb.MessageElement(gplink_str, ldb.FLAG_MOD_ADD, 'gPLink') + + try: + self.samdb.modify(m) + except Exception, e: + raise CommandError("Error adding GPO Link", e) + + self.outf.write("Added/Updated GPO link\n") + cmd_getlink().run(container_dn, H, sambaopts, credopts, versionopts) + + +class cmd_dellink(Command): + """Delete GPO link from a container.""" + + synopsis = "%prog <container_dn> <gpo> [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_args = ['container', 'gpo'] + + takes_options = [ + Option("-H", help="LDB URL for database or target server", type=str), + ] + + def run(self, container, gpo, H=None, sambaopts=None, credopts=None, + versionopts=None): + + self.lp = sambaopts.get_loadparm() + self.creds = credopts.get_credentials(self.lp, fallback_machine=True) + + self.url = dc_url(self.lp, self.creds, H) + + samdb_connect(self) + + # Check if valid GPO + try: + get_gpo_info(self.samdb, gpo=gpo)[0] + except Exception: + raise CommandError("GPO '%s' does not exist" % gpo) + + container_dn = ldb.Dn(self.samdb, container) + del_gpo_link(self.samdb, container_dn, gpo) + self.outf.write("Deleted GPO link.\n") + cmd_getlink().run(container_dn, H, sambaopts, credopts, versionopts) + + +class cmd_listcontainers(Command): + """List all linked containers for a GPO.""" + + synopsis = "%prog <gpo> [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_args = ['gpo'] + + takes_options = [ + Option("-H", help="LDB URL for database or target server", type=str) + ] + + def run(self, gpo, H=None, sambaopts=None, credopts=None, + versionopts=None): + + self.lp = sambaopts.get_loadparm() + self.creds = credopts.get_credentials(self.lp, fallback_machine=True) + + self.url = dc_url(self.lp, self.creds, H) + + samdb_connect(self) + + msg = get_gpo_containers(self.samdb, gpo) + if len(msg): + self.outf.write("Container(s) using GPO %s\n" % gpo) + for m in msg: + self.outf.write(" DN: %s\n" % m['dn']) + else: + self.outf.write("No Containers using GPO %s\n" % gpo) + + +class cmd_getinheritance(Command): + """Get inheritance flag for a container.""" + + synopsis = "%prog <container_dn> [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_args = ['container_dn'] + + takes_options = [ + Option("-H", help="LDB URL for database or target server", type=str) + ] + + def run(self, container_dn, H=None, sambaopts=None, credopts=None, + versionopts=None): + + self.lp = sambaopts.get_loadparm() + self.creds = credopts.get_credentials(self.lp, fallback_machine=True) + + self.url = dc_url(self.lp, self.creds, H) + + samdb_connect(self) + + try: + msg = self.samdb.search(base=container_dn, scope=ldb.SCOPE_BASE, + expression="(objectClass=*)", + attrs=['gPOptions'])[0] + except Exception: + raise CommandError("Container '%s' does not exist" % container_dn) + + inheritance = 0 + if 'gPOptions' in msg: + inheritance = int(msg['gPOptions'][0]) + + if inheritance == dsdb.GPO_BLOCK_INHERITANCE: + self.outf.write("Container has GPO_BLOCK_INHERITANCE\n") + else: + self.outf.write("Container has GPO_INHERIT\n") + + +class cmd_setinheritance(Command): + """Set inheritance flag on a container.""" + + synopsis = "%prog <container_dn> <block|inherit> [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_args = [ 'container_dn', 'inherit_state' ] + + takes_options = [ + Option("-H", help="LDB URL for database or target server", type=str) + ] + + def run(self, container_dn, inherit_state, H=None, sambaopts=None, credopts=None, + versionopts=None): + + if inherit_state.lower() == 'block': + inheritance = dsdb.GPO_BLOCK_INHERITANCE + elif inherit_state.lower() == 'inherit': + inheritance = dsdb.GPO_INHERIT + else: + raise CommandError("Unknown inheritance state (%s)" % inherit_state) + + self.lp = sambaopts.get_loadparm() + self.creds = credopts.get_credentials(self.lp, fallback_machine=True) + + self.url = dc_url(self.lp, self.creds, H) + + samdb_connect(self) + try: + msg = self.samdb.search(base=container_dn, scope=ldb.SCOPE_BASE, + expression="(objectClass=*)", + attrs=['gPOptions'])[0] + except Exception: + raise CommandError("Container '%s' does not exist" % container_dn) + + m = ldb.Message() + m.dn = ldb.Dn(self.samdb, container_dn) + + if 'gPOptions' in msg: + m['new_value'] = ldb.MessageElement(str(inheritance), ldb.FLAG_MOD_REPLACE, 'gPOptions') + else: + m['new_value'] = ldb.MessageElement(str(inheritance), ldb.FLAG_MOD_ADD, 'gPOptions') + + try: + self.samdb.modify(m) + except Exception, e: + raise CommandError("Error setting inheritance state %s" % inherit_state, e) + + +class cmd_fetch(Command): + """Download a GPO.""" + + synopsis = "%prog <gpo> [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_args = ['gpo'] + + takes_options = [ + Option("-H", help="LDB URL for database or target server", type=str), + Option("--tmpdir", help="Temporary directory for copying policy files", type=str) + ] + + def run(self, gpo, H=None, tmpdir=None, sambaopts=None, credopts=None, versionopts=None): + + self.lp = sambaopts.get_loadparm() + self.creds = credopts.get_credentials(self.lp, fallback_machine=True) + + # We need to know writable DC to setup SMB connection + if H and H.startswith('ldap://'): + dc_hostname = H[7:] + self.url = H + else: + dc_hostname = netcmd_finddc(self.lp, self.creds) + self.url = dc_url(self.lp, self.creds, dc=dc_hostname) + + samdb_connect(self) + try: + msg = get_gpo_info(self.samdb, gpo)[0] + except Exception: + raise CommandError("GPO '%s' does not exist" % gpo) + + # verify UNC path + unc = msg['gPCFileSysPath'][0] + try: + [dom_name, service, sharepath] = parse_unc(unc) + except ValueError: + raise CommandError("Invalid GPO path (%s)" % unc) + + # SMB connect to DC + try: + conn = smb.SMB(dc_hostname, service, lp=self.lp, creds=self.creds) + except Exception: + raise CommandError("Error connecting to '%s' using SMB" % dc_hostname) + + # Copy GPT + if tmpdir is None: + tmpdir = "/tmp" + if not os.path.isdir(tmpdir): + raise CommandError("Temoprary directory '%s' does not exist" % tmpdir) + + localdir = os.path.join(tmpdir, "policy") + if not os.path.isdir(localdir): + os.mkdir(localdir) + + gpodir = os.path.join(localdir, gpo) + if os.path.isdir(gpodir): + raise CommandError("GPO directory '%s' already exists, refusing to overwrite" % gpodir) + + try: + os.mkdir(gpodir) + copy_directory_remote_to_local(conn, sharepath, gpodir) + except Exception, e: + # FIXME: Catch more specific exception + raise CommandError("Error copying GPO from DC", e) + self.outf.write('GPO copied to %s\n' % gpodir) + + +class cmd_create(Command): + """Create an empty GPO.""" + + synopsis = "%prog <displayname> [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_args = ['displayname'] + + takes_options = [ + Option("-H", help="LDB URL for database or target server", type=str), + Option("--tmpdir", help="Temporary directory for copying policy files", type=str) + ] + + def run(self, displayname, H=None, tmpdir=None, sambaopts=None, credopts=None, + versionopts=None): + + self.lp = sambaopts.get_loadparm() + self.creds = credopts.get_credentials(self.lp, fallback_machine=True) + + net = Net(creds=self.creds, lp=self.lp) + + # We need to know writable DC to setup SMB connection + if H and H.startswith('ldap://'): + dc_hostname = H[7:] + self.url = H + flags = (nbt.NBT_SERVER_LDAP | + nbt.NBT_SERVER_DS | + nbt.NBT_SERVER_WRITABLE) + cldap_ret = net.finddc(address=dc_hostname, flags=flags) + else: + flags = (nbt.NBT_SERVER_LDAP | + nbt.NBT_SERVER_DS | + nbt.NBT_SERVER_WRITABLE) + cldap_ret = net.finddc(domain=self.lp.get('realm'), flags=flags) + dc_hostname = cldap_ret.pdc_dns_name + self.url = dc_url(self.lp, self.creds, dc=dc_hostname) + + samdb_connect(self) + + msg = get_gpo_info(self.samdb, displayname=displayname) + if msg.count > 0: + raise CommandError("A GPO already existing with name '%s'" % displayname) + + # Create new GUID + guid = str(uuid.uuid4()) + gpo = "{%s}" % guid.upper() + realm = cldap_ret.dns_domain + unc_path = "\\\\%s\\sysvol\\%s\\Policies\\%s" % (realm, realm, gpo) + + # Create GPT + if tmpdir is None: + tmpdir = "/tmp" + if not os.path.isdir(tmpdir): + raise CommandError("Temporary directory '%s' does not exist" % tmpdir) + + localdir = os.path.join(tmpdir, "policy") + if not os.path.isdir(localdir): + os.mkdir(localdir) + + gpodir = os.path.join(localdir, gpo) + if os.path.isdir(gpodir): + raise CommandError("GPO directory '%s' already exists, refusing to overwrite" % gpodir) + + try: + os.mkdir(gpodir) + os.mkdir(os.path.join(gpodir, "Machine")) + os.mkdir(os.path.join(gpodir, "User")) + gpt_contents = "[General]\r\nVersion=0\r\n" + file(os.path.join(gpodir, "GPT.INI"), "w").write(gpt_contents) + except Exception, e: + raise CommandError("Error Creating GPO files", e) + + # Connect to DC over SMB + [dom_name, service, sharepath] = parse_unc(unc_path) + try: + conn = smb.SMB(dc_hostname, service, lp=self.lp, creds=self.creds) + except Exception, e: + raise CommandError("Error connecting to '%s' using SMB" % dc_hostname, e) + + self.samdb.transaction_start() + try: + # Add cn=<guid> + gpo_dn = get_gpo_dn(self.samdb, gpo) + + m = ldb.Message() + m.dn = gpo_dn + m['a01'] = ldb.MessageElement("groupPolicyContainer", ldb.FLAG_MOD_ADD, "objectClass") + self.samdb.add(m) + + # Add cn=User,cn=<guid> + m = ldb.Message() + m.dn = ldb.Dn(self.samdb, "CN=User,%s" % str(gpo_dn)) + m['a01'] = ldb.MessageElement("container", ldb.FLAG_MOD_ADD, "objectClass") + self.samdb.add(m) + + # Add cn=Machine,cn=<guid> + m = ldb.Message() + m.dn = ldb.Dn(self.samdb, "CN=Machine,%s" % str(gpo_dn)) + m['a01'] = ldb.MessageElement("container", ldb.FLAG_MOD_ADD, "objectClass") + self.samdb.add(m) + + # Get new security descriptor + ds_sd_flags = ( security.SECINFO_OWNER | + security.SECINFO_GROUP | + security.SECINFO_DACL ) + msg = get_gpo_info(self.samdb, gpo=gpo, sd_flags=ds_sd_flags)[0] + ds_sd_ndr = msg['nTSecurityDescriptor'][0] + ds_sd = ndr_unpack(security.descriptor, ds_sd_ndr).as_sddl() + + # Create a file system security descriptor + domain_sid = security.dom_sid(self.samdb.get_domain_sid()) + sddl = dsacl2fsacl(ds_sd, domain_sid) + fs_sd = security.descriptor.from_sddl(sddl, domain_sid) + + # Copy GPO directory + create_directory_hier(conn, sharepath) + + # Set ACL + sio = ( security.SECINFO_OWNER | + security.SECINFO_GROUP | + security.SECINFO_DACL | + security.SECINFO_PROTECTED_DACL ) + conn.set_acl(sharepath, fs_sd, sio) + + # Copy GPO files over SMB + copy_directory_local_to_remote(conn, gpodir, sharepath) + + m = ldb.Message() + m.dn = gpo_dn + m['a02'] = ldb.MessageElement(displayname, ldb.FLAG_MOD_REPLACE, "displayName") + m['a03'] = ldb.MessageElement(unc_path, ldb.FLAG_MOD_REPLACE, "gPCFileSysPath") + m['a05'] = ldb.MessageElement("0", ldb.FLAG_MOD_REPLACE, "versionNumber") + m['a07'] = ldb.MessageElement("2", ldb.FLAG_MOD_REPLACE, "gpcFunctionalityVersion") + m['a04'] = ldb.MessageElement("0", ldb.FLAG_MOD_REPLACE, "flags") + controls=["permissive_modify:0"] + self.samdb.modify(m, controls=controls) + except Exception: + self.samdb.transaction_cancel() + raise + else: + self.samdb.transaction_commit() + + self.outf.write("GPO '%s' created as %s\n" % (displayname, gpo)) + + +class cmd_del(Command): + """Delete a GPO.""" + + synopsis = "%prog <gpo> [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_args = ['gpo'] + + takes_options = [ + Option("-H", help="LDB URL for database or target server", type=str), + ] + + def run(self, gpo, H=None, sambaopts=None, credopts=None, + versionopts=None): + + self.lp = sambaopts.get_loadparm() + self.creds = credopts.get_credentials(self.lp, fallback_machine=True) + + # We need to know writable DC to setup SMB connection + if H and H.startswith('ldap://'): + dc_hostname = H[7:] + self.url = H + else: + dc_hostname = netcmd_finddc(self.lp, self.creds) + self.url = dc_url(self.lp, self.creds, dc=dc_hostname) + + samdb_connect(self) + + # Check if valid GPO + try: + msg = get_gpo_info(self.samdb, gpo=gpo)[0] + unc_path = msg['gPCFileSysPath'][0] + except Exception: + raise CommandError("GPO '%s' does not exist" % gpo) + + # Connect to DC over SMB + [dom_name, service, sharepath] = parse_unc(unc_path) + try: + conn = smb.SMB(dc_hostname, service, lp=self.lp, creds=self.creds) + except Exception, e: + raise CommandError("Error connecting to '%s' using SMB" % dc_hostname, e) + + self.samdb.transaction_start() + try: + # Check for existing links + msg = get_gpo_containers(self.samdb, gpo) + + if len(msg): + self.outf.write("GPO %s is linked to containers\n" % gpo) + for m in msg: + del_gpo_link(self.samdb, m['dn'], gpo) + self.outf.write(" Removed link from %s.\n" % m['dn']) + + # Remove LDAP entries + gpo_dn = get_gpo_dn(self.samdb, gpo) + self.samdb.delete(ldb.Dn(self.samdb, "CN=User,%s" % str(gpo_dn))) + self.samdb.delete(ldb.Dn(self.samdb, "CN=Machine,%s" % str(gpo_dn))) + self.samdb.delete(gpo_dn) + + # Remove GPO files + conn.deltree(sharepath) + + except Exception: + self.samdb.transaction_cancel() + raise + else: + self.samdb.transaction_commit() + + self.outf.write("GPO %s deleted.\n" % gpo) + + +class cmd_aclcheck(Command): + """Check all GPOs have matching LDAP and DS ACLs.""" + + synopsis = "%prog [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H") + ] + + def run(self, H=None, sambaopts=None, credopts=None, versionopts=None): + + self.lp = sambaopts.get_loadparm() + self.creds = credopts.get_credentials(self.lp, fallback_machine=True) + + self.url = dc_url(self.lp, self.creds, H) + + # We need to know writable DC to setup SMB connection + if H and H.startswith('ldap://'): + dc_hostname = H[7:] + self.url = H + else: + dc_hostname = netcmd_finddc(self.lp, self.creds) + self.url = dc_url(self.lp, self.creds, dc=dc_hostname) + + samdb_connect(self) + + msg = get_gpo_info(self.samdb, None) + + for m in msg: + # verify UNC path + unc = m['gPCFileSysPath'][0] + try: + [dom_name, service, sharepath] = parse_unc(unc) + except ValueError: + raise CommandError("Invalid GPO path (%s)" % unc) + + # SMB connect to DC + try: + conn = smb.SMB(dc_hostname, service, lp=self.lp, creds=self.creds) + except Exception: + raise CommandError("Error connecting to '%s' using SMB" % dc_hostname) + + fs_sd = conn.get_acl(sharepath, security.SECINFO_OWNER | security.SECINFO_GROUP | security.SECINFO_DACL, security.SEC_FLAG_MAXIMUM_ALLOWED) + + ds_sd_ndr = m['nTSecurityDescriptor'][0] + ds_sd = ndr_unpack(security.descriptor, ds_sd_ndr).as_sddl() + + # Create a file system security descriptor + domain_sid = security.dom_sid(self.samdb.get_domain_sid()) + expected_fs_sddl = dsacl2fsacl(ds_sd, domain_sid) + + if (fs_sd.as_sddl(domain_sid) != expected_fs_sddl): + raise CommandError("Invalid GPO ACL %s on path (%s), should be %s" % (fs_sd.as_sddl(domain_sid), sharepath, expected_fs_sddl)) + + +class cmd_gpo(SuperCommand): + """Group Policy Object (GPO) management.""" + + subcommands = {} + subcommands["listall"] = cmd_listall() + subcommands["list"] = cmd_list() + subcommands["show"] = cmd_show() + subcommands["getlink"] = cmd_getlink() + subcommands["setlink"] = cmd_setlink() + subcommands["dellink"] = cmd_dellink() + subcommands["listcontainers"] = cmd_listcontainers() + subcommands["getinheritance"] = cmd_getinheritance() + subcommands["setinheritance"] = cmd_setinheritance() + subcommands["fetch"] = cmd_fetch() + subcommands["create"] = cmd_create() + subcommands["del"] = cmd_del() + subcommands["aclcheck"] = cmd_aclcheck() diff --git a/python/samba/netcmd/group.py b/python/samba/netcmd/group.py new file mode 100644 index 00000000000..731d4c1564c --- /dev/null +++ b/python/samba/netcmd/group.py @@ -0,0 +1,376 @@ +# Adds a new user to a Samba4 server +# Copyright Jelmer Vernooij 2008 +# +# Based on the original in EJS: +# Copyright Andrew Tridgell 2005 +# +# 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 samba.getopt as options +from samba.netcmd import Command, SuperCommand, CommandError, Option +import ldb +from samba.ndr import ndr_unpack +from samba.dcerpc import security + +from getpass import getpass +from samba.auth import system_session +from samba.samdb import SamDB +from samba.dsdb import ( + GTYPE_SECURITY_DOMAIN_LOCAL_GROUP, + GTYPE_SECURITY_GLOBAL_GROUP, + GTYPE_SECURITY_UNIVERSAL_GROUP, + GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP, + GTYPE_DISTRIBUTION_GLOBAL_GROUP, + GTYPE_DISTRIBUTION_UNIVERSAL_GROUP, +) + +security_group = dict({"Domain": GTYPE_SECURITY_DOMAIN_LOCAL_GROUP, "Global": GTYPE_SECURITY_GLOBAL_GROUP, "Universal": GTYPE_SECURITY_UNIVERSAL_GROUP}) +distribution_group = dict({"Domain": GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP, "Global": GTYPE_DISTRIBUTION_GLOBAL_GROUP, "Universal": GTYPE_DISTRIBUTION_UNIVERSAL_GROUP}) + + +class cmd_group_add(Command): + """Creates a new AD group. + +This command creates a new Active Directory group. The groupname specified on the command is a unique sAMAccountName. + +An Active Directory group may contain user and computer accounts as well as other groups. An administrator creates a group and adds members to that group so they can be managed as a single entity. This helps to simplify security and system administration. + +Groups may also be used to establish email distribution lists, using --group-type=Distribution. + +Groups are located in domains in organizational units (OUs). The group's scope is a characteristic of the group that designates the extent to which the group is applied within the domain tree or forest. + +The group location (OU), type (security or distribution) and scope may all be specified on the samba-tool command when the group is created. + +The command may be run from the root userid or another authorized userid. The +-H or --URL= option can be used to execute the command on a remote server. + +Example1: +samba-tool group add Group1 -H ldap://samba.samdom.example.com --description='Simple group' + +Example1 adds a new group with the name Group1 added to the Users container on a remote LDAP server. The -U parameter is used to pass the userid and password of a user that exists on the remote server and is authorized to issue the command on that server. It defaults to the security type and global scope. + +Example2: +sudo samba-tool group add Group2 --group-type=Distribution + +Example2 adds a new distribution group to the local server. The command is run under root using the sudo command. +""" + + synopsis = "%prog <groupname> [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H"), + Option("--groupou", + help="Alternative location (without domainDN counterpart) to default CN=Users in which new user object will be created", + type=str), + Option("--group-scope", type="choice", choices=["Domain", "Global", "Universal"], + help="Group scope (Domain | Global | Universal)"), + Option("--group-type", type="choice", choices=["Security", "Distribution"], + help="Group type (Security | Distribution)"), + Option("--description", help="Group's description", type=str), + Option("--mail-address", help="Group's email address", type=str), + Option("--notes", help="Groups's notes", type=str), + ] + + takes_args = ["groupname"] + + def run(self, groupname, credopts=None, sambaopts=None, + versionopts=None, H=None, groupou=None, group_scope=None, + group_type=None, description=None, mail_address=None, notes=None): + + if (group_type or "Security") == "Security": + gtype = security_group.get(group_scope, GTYPE_SECURITY_GLOBAL_GROUP) + else: + gtype = distribution_group.get(group_scope, GTYPE_DISTRIBUTION_GLOBAL_GROUP) + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + + try: + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + samdb.newgroup(groupname, groupou=groupou, grouptype = gtype, + description=description, mailaddress=mail_address, notes=notes) + except Exception, e: + # FIXME: catch more specific exception + raise CommandError('Failed to create group "%s"' % groupname, e) + self.outf.write("Added group %s\n" % groupname) + + +class cmd_group_delete(Command): + """Deletes an AD group. + +The command deletes an existing AD group from the Active Directory domain. The groupname specified on the command is the sAMAccountName. + +Deleting a group is a permanent operation. When a group is deleted, all permissions and rights that users in the group had inherited from the group account are deleted as well. + +The command may be run from the root userid or another authorized userid. The -H or --URL option can be used to execute the command on a remote server. + +Example1: +samba-tool group delete Group1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd + +Example1 shows how to delete an AD group from a remote LDAP server. The -U parameter is used to pass the userid and password of a user that exists on the remote server and is authorized to issue the command on that server. + +Example2: +sudo samba-tool group delete Group2 + +Example2 deletes group Group2 from the local server. The command is run under root using the sudo command. +""" + + synopsis = "%prog <groupname> [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H"), + ] + + takes_args = ["groupname"] + + def run(self, groupname, credopts=None, sambaopts=None, versionopts=None, H=None): + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + + try: + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + samdb.deletegroup(groupname) + except Exception, e: + # FIXME: catch more specific exception + raise CommandError('Failed to remove group "%s"' % groupname, e) + self.outf.write("Deleted group %s\n" % groupname) + + +class cmd_group_add_members(Command): + """Add members to an AD group. + +This command adds one or more members to an existing Active Directory group. The command accepts one or more group member names seperated by commas. A group member may be a user or computer account or another Active Directory group. + +When a member is added to a group the member may inherit permissions and rights from the group. Likewise, when permission or rights of a group are changed, the changes may reflect in the members through inheritance. + +Example1: +samba-tool group addmembers supergroup Group1,Group2,User1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd + +Example1 shows how to add two groups, Group1 and Group2 and one user account, User1, to the existing AD group named supergroup. The command will be run on a remote server specified with the -H. The -U parameter is used to pass the userid and password of a user authorized to issue the command on the remote server. + +Example2: +sudo samba-tool group addmembers supergroup User2 + +Example2 shows how to add a single user account, User2, to the supergroup AD group. It uses the sudo command to run as root when issuing the command. +""" + + synopsis = "%prog <groupname> <listofmembers> [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H"), + ] + + takes_args = ["groupname", "listofmembers"] + + def run(self, groupname, listofmembers, credopts=None, sambaopts=None, + versionopts=None, H=None): + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + + try: + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + groupmembers = listofmembers.split(',') + samdb.add_remove_group_members(groupname, groupmembers, + add_members_operation=True) + except Exception, e: + # FIXME: catch more specific exception + raise CommandError('Failed to add members "%s" to group "%s"' % ( + listofmembers, groupname), e) + self.outf.write("Added members to group %s\n" % groupname) + + +class cmd_group_remove_members(Command): + """Remove members from an AD group. + +This command removes one or more members from an existing Active Directory group. The command accepts one or more group member names seperated by commas. A group member may be a user or computer account or another Active Directory group that is a member of the group specified on the command. + +When a member is removed from a group, inherited permissions and rights will no longer apply to the member. + +Example1: +samba-tool group removemembers supergroup Group1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd + +Example1 shows how to remove Group1 from supergroup. The command will run on the remote server specified on the -H parameter. The -U parameter is used to pass the userid and password of a user authorized to issue the command on the remote server. + +Example2: +sudo samba-tool group removemembers supergroup User1 + +Example2 shows how to remove a single user account, User2, from the supergroup AD group. It uses the sudo command to run as root when issuing the command. +""" + + synopsis = "%prog <groupname> <listofmembers> [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H"), + ] + + takes_args = ["groupname", "listofmembers"] + + def run(self, groupname, listofmembers, credopts=None, sambaopts=None, + versionopts=None, H=None): + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + + try: + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + samdb.add_remove_group_members(groupname, listofmembers.split(","), + add_members_operation=False) + except Exception, e: + # FIXME: Catch more specific exception + raise CommandError('Failed to remove members "%s" from group "%s"' % (listofmembers, groupname), e) + self.outf.write("Removed members from group %s\n" % groupname) + + +class cmd_group_list(Command): + """List all groups.""" + + synopsis = "%prog [options]" + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H"), + ] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + def run(self, sambaopts=None, credopts=None, versionopts=None, H=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + + domain_dn = samdb.domain_dn() + res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE, + expression=("(objectClass=group)"), + attrs=["samaccountname"]) + if (len(res) == 0): + return + + for msg in res: + self.outf.write("%s\n" % msg.get("samaccountname", idx=0)) + + +class cmd_group_list_members(Command): + """List all members of an AD group. + +This command lists members from an existing Active Directory group. The command accepts one group name. + +Example1: +samba-tool group listmembers \"Domain Users\" -H ldap://samba.samdom.example.com -Uadministrator%passw0rd +""" + + synopsis = "%prog <groupname> [options]" + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H"), + ] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_args = ["groupname"] + + def run(self, groupname, credopts=None, sambaopts=None, versionopts=None, H=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + + try: + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + + search_filter = "(&(objectClass=group)(samaccountname=%s))" % groupname + res = samdb.search(samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE, + expression=(search_filter), + attrs=["objectSid"]) + + if (len(res) != 1): + return + + group_dn = res[0].get('dn', idx=0) + object_sid = res[0].get('objectSid', idx=0) + + object_sid = ndr_unpack(security.dom_sid, object_sid) + (group_dom_sid, rid) = object_sid.split() + + search_filter = "(|(primaryGroupID=%s)(memberOf=%s))" % (rid, group_dn) + res = samdb.search(samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE, + expression=(search_filter), + attrs=["samAccountName", "cn"]) + + if (len(res) == 0): + return + + for msg in res: + member_name = msg.get("samAccountName", idx=0) + if member_name is None: + member_name = msg.get("cn", idx=0) + self.outf.write("%s\n" % member_name) + + except Exception, e: + raise CommandError('Failed to list members of "%s" group ' % groupname, e) + + +class cmd_group(SuperCommand): + """Group management.""" + + subcommands = {} + subcommands["add"] = cmd_group_add() + subcommands["delete"] = cmd_group_delete() + subcommands["addmembers"] = cmd_group_add_members() + subcommands["removemembers"] = cmd_group_remove_members() + subcommands["list"] = cmd_group_list() + subcommands["listmembers"] = cmd_group_list_members() diff --git a/python/samba/netcmd/ldapcmp.py b/python/samba/netcmd/ldapcmp.py new file mode 100644 index 00000000000..8398205e4ba --- /dev/null +++ b/python/samba/netcmd/ldapcmp.py @@ -0,0 +1,998 @@ +# Unix SMB/CIFS implementation. +# A command to compare differences of objects and attributes between +# two LDAP servers both running at the same time. It generally compares +# one of the three pratitions DOMAIN, CONFIGURATION or SCHEMA. Users +# that have to be provided sheould be able to read objects in any of the +# above partitions. + +# Copyright (C) Zahari Zahariev <zahari.zahariev@postpath.com> 2009, 2010 +# +# 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 os +import re +import sys + +import samba +import samba.getopt as options +from samba import Ldb +from samba.ndr import ndr_unpack +from samba.dcerpc import security +from ldb import SCOPE_SUBTREE, SCOPE_ONELEVEL, SCOPE_BASE, ERR_NO_SUCH_OBJECT, LdbError +from samba.netcmd import ( + Command, + CommandError, + Option, + ) + +global summary +summary = {} + +class LDAPBase(object): + + def __init__(self, host, creds, lp, + two=False, quiet=False, descriptor=False, sort_aces=False, verbose=False, + view="section", base="", scope="SUB", + outf=sys.stdout, errf=sys.stderr): + ldb_options = [] + samdb_url = host + if not "://" in host: + if os.path.isfile(host): + samdb_url = "tdb://%s" % host + else: + samdb_url = "ldap://%s" % host + # use 'paged_search' module when connecting remotely + if samdb_url.lower().startswith("ldap://"): + ldb_options = ["modules:paged_searches"] + self.outf = outf + self.errf = errf + self.ldb = Ldb(url=samdb_url, + credentials=creds, + lp=lp, + options=ldb_options) + self.search_base = base + self.search_scope = scope + self.two_domains = two + self.quiet = quiet + self.descriptor = descriptor + self.sort_aces = sort_aces + self.view = view + self.verbose = verbose + self.host = host + self.base_dn = str(self.ldb.get_default_basedn()) + self.root_dn = str(self.ldb.get_root_basedn()) + self.config_dn = str(self.ldb.get_config_basedn()) + self.schema_dn = str(self.ldb.get_schema_basedn()) + self.domain_netbios = self.find_netbios() + self.server_names = self.find_servers() + self.domain_name = re.sub("[Dd][Cc]=", "", self.base_dn).replace(",", ".") + self.domain_sid = self.find_domain_sid() + self.get_guid_map() + self.get_sid_map() + # + # Log some domain controller specific place-holers that are being used + # when compare content of two DCs. Uncomment for DEBUG purposes. + if self.two_domains and not self.quiet: + self.outf.write("\n* Place-holders for %s:\n" % self.host) + self.outf.write(4*" " + "${DOMAIN_DN} => %s\n" % + self.base_dn) + self.outf.write(4*" " + "${DOMAIN_NETBIOS} => %s\n" % + self.domain_netbios) + self.outf.write(4*" " + "${SERVER_NAME} => %s\n" % + self.server_names) + self.outf.write(4*" " + "${DOMAIN_NAME} => %s\n" % + self.domain_name) + + def find_domain_sid(self): + res = self.ldb.search(base=self.base_dn, expression="(objectClass=*)", scope=SCOPE_BASE) + return ndr_unpack(security.dom_sid,res[0]["objectSid"][0]) + + def find_servers(self): + """ + """ + res = self.ldb.search(base="OU=Domain Controllers,%s" % self.base_dn, + scope=SCOPE_SUBTREE, expression="(objectClass=computer)", attrs=["cn"]) + assert len(res) > 0 + srv = [] + for x in res: + srv.append(x["cn"][0]) + return srv + + def find_netbios(self): + res = self.ldb.search(base="CN=Partitions,%s" % self.config_dn, + scope=SCOPE_SUBTREE, attrs=["nETBIOSName"]) + assert len(res) > 0 + for x in res: + if "nETBIOSName" in x.keys(): + return x["nETBIOSName"][0] + + def object_exists(self, object_dn): + res = None + try: + res = self.ldb.search(base=object_dn, scope=SCOPE_BASE) + except LdbError, (enum, estr): + if enum == ERR_NO_SUCH_OBJECT: + return False + raise + return len(res) == 1 + + def delete_force(self, object_dn): + try: + self.ldb.delete(object_dn) + except Ldb.LdbError, e: + assert "No such object" in str(e) + + def get_attribute_name(self, key): + """ Returns the real attribute name + It resolved ranged results e.g. member;range=0-1499 + """ + + r = re.compile("^([^;]+);range=(\d+)-(\d+|\*)$") + + m = r.match(key) + if m is None: + return key + + return m.group(1) + + def get_attribute_values(self, object_dn, key, vals): + """ Returns list with all attribute values + It resolved ranged results e.g. member;range=0-1499 + """ + + r = re.compile("^([^;]+);range=(\d+)-(\d+|\*)$") + + m = r.match(key) + if m is None: + # no range, just return the values + return vals + + attr = m.group(1) + hi = int(m.group(3)) + + # get additional values in a loop + # until we get a response with '*' at the end + while True: + + n = "%s;range=%d-*" % (attr, hi + 1) + res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=[n]) + assert len(res) == 1 + res = dict(res[0]) + del res["dn"] + + fm = None + fvals = None + + for key in res.keys(): + m = r.match(key) + + if m is None: + continue + + if m.group(1) != attr: + continue + + fm = m + fvals = list(res[key]) + break + + if fm is None: + break + + vals.extend(fvals) + if fm.group(3) == "*": + # if we got "*" we're done + break + + assert int(fm.group(2)) == hi + 1 + hi = int(fm.group(3)) + + return vals + + def get_attributes(self, object_dn): + """ Returns dict with all default visible attributes + """ + res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["*"]) + assert len(res) == 1 + res = dict(res[0]) + # 'Dn' element is not iterable and we have it as 'distinguishedName' + del res["dn"] + for key in res.keys(): + vals = list(res[key]) + del res[key] + name = self.get_attribute_name(key) + res[name] = self.get_attribute_values(object_dn, key, vals) + + return res + + def get_descriptor_sddl(self, object_dn): + res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["nTSecurityDescriptor"]) + desc = res[0]["nTSecurityDescriptor"][0] + desc = ndr_unpack(security.descriptor, desc) + return desc.as_sddl(self.domain_sid) + + def guid_as_string(self, guid_blob): + """ Translate binary representation of schemaIDGUID to standard string representation. + @gid_blob: binary schemaIDGUID + """ + blob = "%s" % guid_blob + stops = [4, 2, 2, 2, 6] + index = 0 + res = "" + x = 0 + while x < len(stops): + tmp = "" + y = 0 + while y < stops[x]: + c = hex(ord(blob[index])).replace("0x", "") + c = [None, "0" + c, c][len(c)] + if 2 * index < len(blob): + tmp = c + tmp + else: + tmp += c + index += 1 + y += 1 + res += tmp + " " + x += 1 + assert index == len(blob) + return res.strip().replace(" ", "-") + + def get_guid_map(self): + """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights. + """ + self.guid_map = {} + res = self.ldb.search(base=self.schema_dn, + expression="(schemaIdGuid=*)", scope=SCOPE_SUBTREE, attrs=["schemaIdGuid", "name"]) + for item in res: + self.guid_map[self.guid_as_string(item["schemaIdGuid"]).lower()] = item["name"][0] + # + res = self.ldb.search(base="cn=extended-rights,%s" % self.config_dn, + expression="(rightsGuid=*)", scope=SCOPE_SUBTREE, attrs=["rightsGuid", "name"]) + for item in res: + self.guid_map[str(item["rightsGuid"]).lower()] = item["name"][0] + + def get_sid_map(self): + """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights. + """ + self.sid_map = {} + res = self.ldb.search(base=self.base_dn, + expression="(objectSid=*)", scope=SCOPE_SUBTREE, attrs=["objectSid", "sAMAccountName"]) + for item in res: + try: + self.sid_map["%s" % ndr_unpack(security.dom_sid, item["objectSid"][0])] = item["sAMAccountName"][0] + except KeyError: + pass + +class Descriptor(object): + def __init__(self, connection, dn, outf=sys.stdout, errf=sys.stderr): + self.outf = outf + self.errf = errf + self.con = connection + self.dn = dn + self.sddl = self.con.get_descriptor_sddl(self.dn) + self.dacl_list = self.extract_dacl() + if self.con.sort_aces: + self.dacl_list.sort() + + def extract_dacl(self): + """ Extracts the DACL as a list of ACE string (with the brakets). + """ + try: + if "S:" in self.sddl: + res = re.search("D:(.*?)(\(.*?\))S:", self.sddl).group(2) + else: + res = re.search("D:(.*?)(\(.*\))", self.sddl).group(2) + except AttributeError: + return [] + return re.findall("(\(.*?\))", res) + + def fix_guid(self, ace): + res = "%s" % ace + guids = re.findall("[a-z0-9]+?-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+", res) + # If there are not GUIDs to replace return the same ACE + if len(guids) == 0: + return res + for guid in guids: + try: + name = self.con.guid_map[guid.lower()] + res = res.replace(guid, name) + except KeyError: + # Do not bother if the GUID is not found in + # cn=Schema or cn=Extended-Rights + pass + return res + + def fix_sid(self, ace): + res = "%s" % ace + sids = re.findall("S-[-0-9]+", res) + # If there are not SIDs to replace return the same ACE + if len(sids) == 0: + return res + for sid in sids: + try: + name = self.con.sid_map[sid] + res = res.replace(sid, name) + except KeyError: + # Do not bother if the SID is not found in baseDN + pass + return res + + def fixit(self, ace): + """ Combine all replacement methods in one + """ + res = "%s" % ace + res = self.fix_guid(res) + res = self.fix_sid(res) + return res + + def diff_1(self, other): + res = "" + if len(self.dacl_list) != len(other.dacl_list): + res += 4*" " + "Difference in ACE count:\n" + res += 8*" " + "=> %s\n" % len(self.dacl_list) + res += 8*" " + "=> %s\n" % len(other.dacl_list) + # + i = 0 + flag = True + while True: + self_ace = None + other_ace = None + try: + self_ace = "%s" % self.dacl_list[i] + except IndexError: + self_ace = "" + # + try: + other_ace = "%s" % other.dacl_list[i] + except IndexError: + other_ace = "" + if len(self_ace) + len(other_ace) == 0: + break + self_ace_fixed = "%s" % self.fixit(self_ace) + other_ace_fixed = "%s" % other.fixit(other_ace) + if self_ace_fixed != other_ace_fixed: + res += "%60s * %s\n" % ( self_ace_fixed, other_ace_fixed ) + flag = False + else: + res += "%60s | %s\n" % ( self_ace_fixed, other_ace_fixed ) + i += 1 + return (flag, res) + + def diff_2(self, other): + res = "" + if len(self.dacl_list) != len(other.dacl_list): + res += 4*" " + "Difference in ACE count:\n" + res += 8*" " + "=> %s\n" % len(self.dacl_list) + res += 8*" " + "=> %s\n" % len(other.dacl_list) + # + common_aces = [] + self_aces = [] + other_aces = [] + self_dacl_list_fixed = [] + other_dacl_list_fixed = [] + [self_dacl_list_fixed.append( self.fixit(ace) ) for ace in self.dacl_list] + [other_dacl_list_fixed.append( other.fixit(ace) ) for ace in other.dacl_list] + for ace in self_dacl_list_fixed: + try: + other_dacl_list_fixed.index(ace) + except ValueError: + self_aces.append(ace) + else: + common_aces.append(ace) + self_aces = sorted(self_aces) + if len(self_aces) > 0: + res += 4*" " + "ACEs found only in %s:\n" % self.con.host + for ace in self_aces: + res += 8*" " + ace + "\n" + # + for ace in other_dacl_list_fixed: + try: + self_dacl_list_fixed.index(ace) + except ValueError: + other_aces.append(ace) + else: + common_aces.append(ace) + other_aces = sorted(other_aces) + if len(other_aces) > 0: + res += 4*" " + "ACEs found only in %s:\n" % other.con.host + for ace in other_aces: + res += 8*" " + ace + "\n" + # + common_aces = sorted(list(set(common_aces))) + if self.con.verbose: + res += 4*" " + "ACEs found in both:\n" + for ace in common_aces: + res += 8*" " + ace + "\n" + return (self_aces == [] and other_aces == [], res) + +class LDAPObject(object): + def __init__(self, connection, dn, summary, filter_list, + outf=sys.stdout, errf=sys.stderr): + self.outf = outf + self.errf = errf + self.con = connection + self.two_domains = self.con.two_domains + self.quiet = self.con.quiet + self.verbose = self.con.verbose + self.summary = summary + self.dn = dn.replace("${DOMAIN_DN}", self.con.base_dn) + self.dn = self.dn.replace("CN=${DOMAIN_NETBIOS}", "CN=%s" % self.con.domain_netbios) + for x in self.con.server_names: + self.dn = self.dn.replace("CN=${SERVER_NAME}", "CN=%s" % x) + self.attributes = self.con.get_attributes(self.dn) + # Attributes that are considered always to be different e.g based on timestamp etc. + # + # One domain - two domain controllers + self.ignore_attributes = [ + # Default Naming Context + "lastLogon", "lastLogoff", "badPwdCount", "logonCount", "badPasswordTime", "modifiedCount", + "operatingSystemVersion","oEMInformation", + "ridNextRID", "rIDPreviousAllocationPool", + # Configuration Naming Context + "repsFrom", "dSCorePropagationData", "msExchServer1HighestUSN", + "replUpToDateVector", "repsTo", "whenChanged", "uSNChanged", "uSNCreated", + # Schema Naming Context + "prefixMap"] + if filter_list: + self.ignore_attributes += filter_list + + self.dn_attributes = [] + self.domain_attributes = [] + self.servername_attributes = [] + self.netbios_attributes = [] + self.other_attributes = [] + # Two domains - two domain controllers + + if self.two_domains: + self.ignore_attributes += [ + "objectCategory", "objectGUID", "objectSid", "whenCreated", "pwdLastSet", "uSNCreated", "creationTime", + "modifiedCount", "priorSetTime", "rIDManagerReference", "gPLink", "ipsecNFAReference", + "fRSPrimaryMember", "fSMORoleOwner", "masteredBy", "ipsecOwnersReference", "wellKnownObjects", + "badPwdCount", "ipsecISAKMPReference", "ipsecFilterReference", "msDs-masteredBy", "lastSetTime", + "ipsecNegotiationPolicyReference", "subRefs", "gPCFileSysPath", "accountExpires", "invocationId", + # After Exchange preps + "targetAddress", "msExchMailboxGuid", "siteFolderGUID"] + # + # Attributes that contain the unique DN tail part e.g. 'DC=samba,DC=org' + self.dn_attributes = [ + "distinguishedName", "defaultObjectCategory", "member", "memberOf", "siteList", "nCName", + "homeMDB", "homeMTA", "interSiteTopologyGenerator", "serverReference", + "msDS-HasInstantiatedNCs", "hasMasterNCs", "msDS-hasMasterNCs", "msDS-HasDomainNCs", "dMDLocation", + "msDS-IsDomainFor", "rIDSetReferences", "serverReferenceBL", + # After Exchange preps + "msExchHomeRoutingGroup", "msExchResponsibleMTAServer", "siteFolderServer", "msExchRoutingMasterDN", + "msExchRoutingGroupMembersBL", "homeMDBBL", "msExchHomePublicMDB", "msExchOwningServer", "templateRoots", + "addressBookRoots", "msExchPolicyRoots", "globalAddressList", "msExchOwningPFTree", + "msExchResponsibleMTAServerBL", "msExchOwningPFTreeBL",] + self.dn_attributes = [x.upper() for x in self.dn_attributes] + # + # Attributes that contain the Domain name e.g. 'samba.org' + self.domain_attributes = [ + "proxyAddresses", "mail", "userPrincipalName", "msExchSmtpFullyQualifiedDomainName", + "dnsHostName", "networkAddress", "dnsRoot", "servicePrincipalName",] + self.domain_attributes = [x.upper() for x in self.domain_attributes] + # + # May contain DOMAIN_NETBIOS and SERVER_NAME + self.servername_attributes = [ "distinguishedName", "name", "CN", "sAMAccountName", "dNSHostName", + "servicePrincipalName", "rIDSetReferences", "serverReference", "serverReferenceBL", + "msDS-IsDomainFor", "interSiteTopologyGenerator",] + self.servername_attributes = [x.upper() for x in self.servername_attributes] + # + self.netbios_attributes = [ "servicePrincipalName", "CN", "distinguishedName", "nETBIOSName", "name",] + self.netbios_attributes = [x.upper() for x in self.netbios_attributes] + # + self.other_attributes = [ "name", "DC",] + self.other_attributes = [x.upper() for x in self.other_attributes] + # + self.ignore_attributes = [x.upper() for x in self.ignore_attributes] + + def log(self, msg): + """ + Log on the screen if there is no --quiet oprion set + """ + if not self.quiet: + self.outf.write(msg+"\n") + + def fix_dn(self, s): + res = "%s" % s + if not self.two_domains: + return res + if res.upper().endswith(self.con.base_dn.upper()): + res = res[:len(res)-len(self.con.base_dn)] + "${DOMAIN_DN}" + return res + + def fix_domain_name(self, s): + res = "%s" % s + if not self.two_domains: + return res + res = res.replace(self.con.domain_name.lower(), self.con.domain_name.upper()) + res = res.replace(self.con.domain_name.upper(), "${DOMAIN_NAME}") + return res + + def fix_domain_netbios(self, s): + res = "%s" % s + if not self.two_domains: + return res + res = res.replace(self.con.domain_netbios.lower(), self.con.domain_netbios.upper()) + res = res.replace(self.con.domain_netbios.upper(), "${DOMAIN_NETBIOS}") + return res + + def fix_server_name(self, s): + res = "%s" % s + if not self.two_domains or len(self.con.server_names) > 1: + return res + for x in self.con.server_names: + res = res.upper().replace(x, "${SERVER_NAME}") + return res + + def __eq__(self, other): + if self.con.descriptor: + return self.cmp_desc(other) + return self.cmp_attrs(other) + + def cmp_desc(self, other): + d1 = Descriptor(self.con, self.dn, outf=self.outf, errf=self.errf) + d2 = Descriptor(other.con, other.dn, outf=self.outf, errf=self.errf) + if self.con.view == "section": + res = d1.diff_2(d2) + elif self.con.view == "collision": + res = d1.diff_1(d2) + else: + raise Exception("Unknown --view option value.") + # + self.screen_output = res[1][:-1] + other.screen_output = res[1][:-1] + # + return res[0] + + def cmp_attrs(self, other): + res = "" + self.unique_attrs = [] + self.df_value_attrs = [] + other.unique_attrs = [] + if self.attributes.keys() != other.attributes.keys(): + # + title = 4*" " + "Attributes found only in %s:" % self.con.host + for x in self.attributes.keys(): + if not x in other.attributes.keys() and \ + not x.upper() in [q.upper() for q in other.ignore_attributes]: + if title: + res += title + "\n" + title = None + res += 8*" " + x + "\n" + self.unique_attrs.append(x) + # + title = 4*" " + "Attributes found only in %s:" % other.con.host + for x in other.attributes.keys(): + if not x in self.attributes.keys() and \ + not x.upper() in [q.upper() for q in self.ignore_attributes]: + if title: + res += title + "\n" + title = None + res += 8*" " + x + "\n" + other.unique_attrs.append(x) + # + missing_attrs = [x.upper() for x in self.unique_attrs] + missing_attrs += [x.upper() for x in other.unique_attrs] + title = 4*" " + "Difference in attribute values:" + for x in self.attributes.keys(): + if x.upper() in self.ignore_attributes or x.upper() in missing_attrs: + continue + if isinstance(self.attributes[x], list) and isinstance(other.attributes[x], list): + self.attributes[x] = sorted(self.attributes[x]) + other.attributes[x] = sorted(other.attributes[x]) + if self.attributes[x] != other.attributes[x]: + p = None + q = None + m = None + n = None + # First check if the difference can be fixed but shunting the first part + # of the DomainHostName e.g. 'mysamba4.test.local' => 'mysamba4' + if x.upper() in self.other_attributes: + p = [self.con.domain_name.split(".")[0] == j for j in self.attributes[x]] + q = [other.con.domain_name.split(".")[0] == j for j in other.attributes[x]] + if p == q: + continue + # Attribute values that are list that contain DN based values that may differ + elif x.upper() in self.dn_attributes: + m = p + n = q + if not p and not q: + m = self.attributes[x] + n = other.attributes[x] + p = [self.fix_dn(j) for j in m] + q = [other.fix_dn(j) for j in n] + if p == q: + continue + # Attributes that contain the Domain name in them + if x.upper() in self.domain_attributes: + m = p + n = q + if not p and not q: + m = self.attributes[x] + n = other.attributes[x] + p = [self.fix_domain_name(j) for j in m] + q = [other.fix_domain_name(j) for j in n] + if p == q: + continue + # + if x.upper() in self.servername_attributes: + # Attributes with SERVER_NAME + m = p + n = q + if not p and not q: + m = self.attributes[x] + n = other.attributes[x] + p = [self.fix_server_name(j) for j in m] + q = [other.fix_server_name(j) for j in n] + if p == q: + continue + # + if x.upper() in self.netbios_attributes: + # Attributes with NETBIOS Domain name + m = p + n = q + if not p and not q: + m = self.attributes[x] + n = other.attributes[x] + p = [self.fix_domain_netbios(j) for j in m] + q = [other.fix_domain_netbios(j) for j in n] + if p == q: + continue + # + if title: + res += title + "\n" + title = None + if p and q: + res += 8*" " + x + " => \n%s\n%s" % (p, q) + "\n" + else: + res += 8*" " + x + " => \n%s\n%s" % (self.attributes[x], other.attributes[x]) + "\n" + self.df_value_attrs.append(x) + # + if self.unique_attrs + other.unique_attrs != []: + assert self.unique_attrs != other.unique_attrs + self.summary["unique_attrs"] += self.unique_attrs + self.summary["df_value_attrs"] += self.df_value_attrs + other.summary["unique_attrs"] += other.unique_attrs + other.summary["df_value_attrs"] += self.df_value_attrs # they are the same + # + self.screen_output = res[:-1] + other.screen_output = res[:-1] + # + return res == "" + + +class LDAPBundel(object): + + def __init__(self, connection, context, dn_list=None, filter_list=None, + outf=sys.stdout, errf=sys.stderr): + self.outf = outf + self.errf = errf + self.con = connection + self.two_domains = self.con.two_domains + self.quiet = self.con.quiet + self.verbose = self.con.verbose + self.search_base = self.con.search_base + self.search_scope = self.con.search_scope + self.summary = {} + self.summary["unique_attrs"] = [] + self.summary["df_value_attrs"] = [] + self.summary["known_ignored_dn"] = [] + self.summary["abnormal_ignored_dn"] = [] + self.filter_list = filter_list + if dn_list: + self.dn_list = dn_list + elif context.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]: + self.context = context.upper() + self.dn_list = self.get_dn_list(context) + else: + raise Exception("Unknown initialization data for LDAPBundel().") + counter = 0 + while counter < len(self.dn_list) and self.two_domains: + # Use alias reference + tmp = self.dn_list[counter] + tmp = tmp[:len(tmp)-len(self.con.base_dn)] + "${DOMAIN_DN}" + tmp = tmp.replace("CN=%s" % self.con.domain_netbios, "CN=${DOMAIN_NETBIOS}") + if len(self.con.server_names) == 1: + for x in self.con.server_names: + tmp = tmp.replace("CN=%s" % x, "CN=${SERVER_NAME}") + self.dn_list[counter] = tmp + counter += 1 + self.dn_list = list(set(self.dn_list)) + self.dn_list = sorted(self.dn_list) + self.size = len(self.dn_list) + + def log(self, msg): + """ + Log on the screen if there is no --quiet oprion set + """ + if not self.quiet: + self.outf.write(msg+"\n") + + def update_size(self): + self.size = len(self.dn_list) + self.dn_list = sorted(self.dn_list) + + def __eq__(self, other): + res = True + if self.size != other.size: + self.log( "\n* DN lists have different size: %s != %s" % (self.size, other.size) ) + res = False + # + # This is the case where we want to explicitly compare two objects with different DNs. + # It does not matter if they are in the same DC, in two DC in one domain or in two + # different domains. + if self.search_scope != SCOPE_BASE: + title= "\n* DNs found only in %s:" % self.con.host + for x in self.dn_list: + if not x.upper() in [q.upper() for q in other.dn_list]: + if title: + self.log( title ) + title = None + res = False + self.log( 4*" " + x ) + self.dn_list[self.dn_list.index(x)] = "" + self.dn_list = [x for x in self.dn_list if x] + # + title= "\n* DNs found only in %s:" % other.con.host + for x in other.dn_list: + if not x.upper() in [q.upper() for q in self.dn_list]: + if title: + self.log( title ) + title = None + res = False + self.log( 4*" " + x ) + other.dn_list[other.dn_list.index(x)] = "" + other.dn_list = [x for x in other.dn_list if x] + # + self.update_size() + other.update_size() + assert self.size == other.size + assert sorted([x.upper() for x in self.dn_list]) == sorted([x.upper() for x in other.dn_list]) + self.log( "\n* Objects to be compared: %s" % self.size ) + + index = 0 + while index < self.size: + skip = False + try: + object1 = LDAPObject(connection=self.con, + dn=self.dn_list[index], + summary=self.summary, + filter_list=self.filter_list, + outf=self.outf, errf=self.errf) + except LdbError, (enum, estr): + if enum == ERR_NO_SUCH_OBJECT: + self.log( "\n!!! Object not found: %s" % self.dn_list[index] ) + skip = True + raise + try: + object2 = LDAPObject(connection=other.con, + dn=other.dn_list[index], + summary=other.summary, + filter_list=self.filter_list, + outf=self.outf, errf=self.errf) + except LdbError, (enum, estr): + if enum == ERR_NO_SUCH_OBJECT: + self.log( "\n!!! Object not found: %s" % other.dn_list[index] ) + skip = True + raise + if skip: + index += 1 + continue + if object1 == object2: + if self.con.verbose: + self.log( "\nComparing:" ) + self.log( "'%s' [%s]" % (object1.dn, object1.con.host) ) + self.log( "'%s' [%s]" % (object2.dn, object2.con.host) ) + self.log( 4*" " + "OK" ) + else: + self.log( "\nComparing:" ) + self.log( "'%s' [%s]" % (object1.dn, object1.con.host) ) + self.log( "'%s' [%s]" % (object2.dn, object2.con.host) ) + self.log( object1.screen_output ) + self.log( 4*" " + "FAILED" ) + res = False + self.summary = object1.summary + other.summary = object2.summary + index += 1 + # + return res + + def get_dn_list(self, context): + """ Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema. + Parse all DNs and filter those that are 'strange' or abnormal. + """ + if context.upper() == "DOMAIN": + search_base = self.con.base_dn + elif context.upper() == "CONFIGURATION": + search_base = self.con.config_dn + elif context.upper() == "SCHEMA": + search_base = self.con.schema_dn + elif context.upper() == "DNSDOMAIN": + search_base = "DC=DomainDnsZones,%s" % self.con.base_dn + elif context.upper() == "DNSFOREST": + search_base = "DC=ForestDnsZones,%s" % self.con.root_dn + + dn_list = [] + if not self.search_base: + self.search_base = search_base + self.search_scope = self.search_scope.upper() + if self.search_scope == "SUB": + self.search_scope = SCOPE_SUBTREE + elif self.search_scope == "BASE": + self.search_scope = SCOPE_BASE + elif self.search_scope == "ONE": + self.search_scope = SCOPE_ONELEVEL + else: + raise StandardError("Wrong 'scope' given. Choose from: SUB, ONE, BASE") + try: + res = self.con.ldb.search(base=self.search_base, scope=self.search_scope, attrs=["dn"]) + except LdbError, (enum, estr): + self.outf.write("Failed search of base=%s\n" % self.search_base) + raise + for x in res: + dn_list.append(x["dn"].get_linearized()) + # + global summary + # + return dn_list + + def print_summary(self): + self.summary["unique_attrs"] = list(set(self.summary["unique_attrs"])) + self.summary["df_value_attrs"] = list(set(self.summary["df_value_attrs"])) + # + if self.summary["unique_attrs"]: + self.log( "\nAttributes found only in %s:" % self.con.host ) + self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["unique_attrs"]]) ) + # + if self.summary["df_value_attrs"]: + self.log( "\nAttributes with different values:" ) + self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["df_value_attrs"]]) ) + self.summary["df_value_attrs"] = [] + + +class cmd_ldapcmp(Command): + """Compare two ldap databases.""" + synopsis = "%prog <URL1> <URL2> (domain|configuration|schema|dnsdomain|dnsforest) [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptionsDouble, + } + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptionsDouble, + } + + takes_args = ["URL1", "URL2", "context1?", "context2?", "context3?"] + + takes_options = [ + Option("-w", "--two", dest="two", action="store_true", default=False, + help="Hosts are in two different domains"), + Option("-q", "--quiet", dest="quiet", action="store_true", default=False, + help="Do not print anything but relay on just exit code"), + Option("-v", "--verbose", dest="verbose", action="store_true", default=False, + help="Print all DN pairs that have been compared"), + Option("--sd", dest="descriptor", action="store_true", default=False, + help="Compare nTSecurityDescriptor attibutes only"), + Option("--sort-aces", dest="sort_aces", action="store_true", default=False, + help="Sort ACEs before comparison of nTSecurityDescriptor attribute"), + Option("--view", dest="view", default="section", + help="Display mode for nTSecurityDescriptor results. Possible values: section or collision."), + Option("--base", dest="base", default="", + help="Pass search base that will build DN list for the first DC."), + Option("--base2", dest="base2", default="", + help="Pass search base that will build DN list for the second DC. Used when --two or when compare two different DNs."), + Option("--scope", dest="scope", default="SUB", + help="Pass search scope that builds DN list. Options: SUB, ONE, BASE"), + Option("--filter", dest="filter", default="", + help="List of comma separated attributes to ignore in the comparision"), + ] + + def run(self, URL1, URL2, + context1=None, context2=None, context3=None, + two=False, quiet=False, verbose=False, descriptor=False, sort_aces=False, + view="section", base="", base2="", scope="SUB", filter="", + credopts=None, sambaopts=None, versionopts=None): + + lp = sambaopts.get_loadparm() + + using_ldap = URL1.startswith("ldap") or URL2.startswith("ldap") + + if using_ldap: + creds = credopts.get_credentials(lp, fallback_machine=True) + else: + creds = None + creds2 = credopts.get_credentials2(lp, guess=False) + if creds2.is_anonymous(): + creds2 = creds + else: + creds2.set_domain("") + creds2.set_workstation("") + if using_ldap and not creds.authentication_requested(): + raise CommandError("You must supply at least one username/password pair") + + # make a list of contexts to compare in + contexts = [] + if context1 is None: + if base and base2: + # If search bases are specified context is defaulted to + # DOMAIN so the given search bases can be verified. + contexts = ["DOMAIN"] + else: + # if no argument given, we compare all contexts + contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA"] + else: + for c in [context1, context2, context3]: + if c is None: + continue + if not c.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]: + raise CommandError("Incorrect argument: %s" % c) + contexts.append(c.upper()) + + if verbose and quiet: + raise CommandError("You cannot set --verbose and --quiet together") + if (not base and base2) or (base and not base2): + raise CommandError("You need to specify both --base and --base2 at the same time") + if descriptor and view.upper() not in ["SECTION", "COLLISION"]: + raise CommandError("Invalid --view value. Choose from: section or collision") + if not scope.upper() in ["SUB", "ONE", "BASE"]: + raise CommandError("Invalid --scope value. Choose from: SUB, ONE, BASE") + + con1 = LDAPBase(URL1, creds, lp, + two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces, + verbose=verbose,view=view, base=base, scope=scope, + outf=self.outf, errf=self.errf) + assert len(con1.base_dn) > 0 + + con2 = LDAPBase(URL2, creds2, lp, + two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces, + verbose=verbose, view=view, base=base2, scope=scope, + outf=self.outf, errf=self.errf) + assert len(con2.base_dn) > 0 + + filter_list = filter.split(",") + + status = 0 + for context in contexts: + if not quiet: + self.outf.write("\n* Comparing [%s] context...\n" % context) + + b1 = LDAPBundel(con1, context=context, filter_list=filter_list, + outf=self.outf, errf=self.errf) + b2 = LDAPBundel(con2, context=context, filter_list=filter_list, + outf=self.outf, errf=self.errf) + + if b1 == b2: + if not quiet: + self.outf.write("\n* Result for [%s]: SUCCESS\n" % + context) + else: + if not quiet: + self.outf.write("\n* Result for [%s]: FAILURE\n" % context) + if not descriptor: + assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"]) + b2.summary["df_value_attrs"] = [] + self.outf.write("\nSUMMARY\n") + self.outf.write("---------\n") + b1.print_summary() + b2.print_summary() + # mark exit status as FAILURE if a least one comparison failed + status = -1 + if status != 0: + raise CommandError("Compare failed: %d" % status) diff --git a/python/samba/netcmd/main.py b/python/samba/netcmd/main.py new file mode 100644 index 00000000000..5f788235880 --- /dev/null +++ b/python/samba/netcmd/main.py @@ -0,0 +1,70 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 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/>. +# + +"""The main samba-tool command implementation.""" + +from samba import getopt as options + +from samba.netcmd import SuperCommand +from samba.netcmd.dbcheck import cmd_dbcheck +from samba.netcmd.delegation import cmd_delegation +from samba.netcmd.dns import cmd_dns +from samba.netcmd.domain import cmd_domain +from samba.netcmd.drs import cmd_drs +from samba.netcmd.dsacl import cmd_dsacl +from samba.netcmd.fsmo import cmd_fsmo +from samba.netcmd.gpo import cmd_gpo +from samba.netcmd.group import cmd_group +from samba.netcmd.ldapcmp import cmd_ldapcmp +from samba.netcmd.ntacl import cmd_ntacl +from samba.netcmd.rodc import cmd_rodc +from samba.netcmd.sites import cmd_sites +from samba.netcmd.spn import cmd_spn +from samba.netcmd.testparm import cmd_testparm +from samba.netcmd.time import cmd_time +from samba.netcmd.user import cmd_user +from samba.netcmd.vampire import cmd_vampire +from samba.netcmd.processes import cmd_processes + + +class cmd_sambatool(SuperCommand): + """Main samba administration tool.""" + + takes_optiongroups = { + "versionopts": options.VersionOptions, + } + + subcommands = {} + subcommands["dbcheck"] = cmd_dbcheck() + subcommands["delegation"] = cmd_delegation() + subcommands["dns"] = cmd_dns() + subcommands["domain"] = cmd_domain() + subcommands["drs"] = cmd_drs() + subcommands["dsacl"] = cmd_dsacl() + subcommands["fsmo"] = cmd_fsmo() + subcommands["gpo"] = cmd_gpo() + subcommands["group"] = cmd_group() + subcommands["ldapcmp"] = cmd_ldapcmp() + subcommands["ntacl"] = cmd_ntacl() + subcommands["rodc"] = cmd_rodc() + subcommands["sites"] = cmd_sites() + subcommands["spn"] = cmd_spn() + subcommands["testparm"] = cmd_testparm() + subcommands["time"] = cmd_time() + subcommands["user"] = cmd_user() + subcommands["vampire"] = cmd_vampire() + subcommands["processes"] = cmd_processes() diff --git a/python/samba/netcmd/ntacl.py b/python/samba/netcmd/ntacl.py new file mode 100644 index 00000000000..6d4d350653e --- /dev/null +++ b/python/samba/netcmd/ntacl.py @@ -0,0 +1,260 @@ +# Manipulate file NT ACLs +# +# Copyright Matthieu Patou 2010 <mat@matws.net> +# +# 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/>. +# + +from samba.credentials import DONT_USE_KERBEROS +import samba.getopt as options +from samba.dcerpc import security, idmap +from samba.ntacls import setntacl, getntacl +from samba import Ldb +from samba.ndr import ndr_unpack, ndr_print +from samba.samdb import SamDB +from samba.samba3 import param as s3param, passdb, smbd +from samba import provision + +from ldb import SCOPE_BASE +import os + +from samba.auth import system_session +from samba.netcmd import ( + Command, + CommandError, + SuperCommand, + Option, + ) + + + +class cmd_ntacl_set(Command): + """Set ACLs on a file.""" + + synopsis = "%prog <acl> <file> [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_options = [ + Option("--quiet", help="Be quiet", action="store_true"), + Option("--xattr-backend", type="choice", help="xattr backend type (native fs or tdb)", + choices=["native","tdb"]), + Option("--eadb-file", help="Name of the tdb file where attributes are stored", type="string"), + Option("--use-ntvfs", help="Set the ACLs directly to the TDB or xattr for use with the ntvfs file server", action="store_true"), + Option("--use-s3fs", help="Set the ACLs for use with the default s3fs file server via the VFS layer", action="store_true"), + Option("--service", help="Name of the smb.conf service to use when applying the ACLs", type="string") + ] + + takes_args = ["acl","file"] + + def run(self, acl, file, use_ntvfs=False, use_s3fs=False, + quiet=False,xattr_backend=None,eadb_file=None, + credopts=None, sambaopts=None, versionopts=None, + service=None): + logger = self.get_logger() + lp = sambaopts.get_loadparm() + try: + samdb = SamDB(session_info=system_session(), + lp=lp) + except Exception, e: + raise CommandError("Unable to open samdb:", e) + + if not use_ntvfs and not use_s3fs: + use_ntvfs = "smb" in lp.get("server services") + elif use_s3fs: + use_ntvfs = False + + try: + domain_sid = security.dom_sid(samdb.domain_sid) + except: + raise CommandError("Unable to read domain SID from configuration files") + + s3conf = s3param.get_context() + s3conf.load(lp.configfile) + # ensure we are using the right samba_dsdb passdb backend, no matter what + s3conf.set("passdb backend", "samba_dsdb:%s" % samdb.url) + + setntacl(lp, file, acl, str(domain_sid), xattr_backend, eadb_file, use_ntvfs=use_ntvfs, service=service) + + if use_ntvfs: + logger.warning("Please note that POSIX permissions have NOT been changed, only the stored NT ACL") + + +class cmd_ntacl_get(Command): + """Get ACLs of a file.""" + synopsis = "%prog <file> [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_options = [ + Option("--as-sddl", help="Output ACL in the SDDL format", action="store_true"), + Option("--xattr-backend", type="choice", help="xattr backend type (native fs or tdb)", + choices=["native","tdb"]), + Option("--eadb-file", help="Name of the tdb file where attributes are stored", type="string"), + Option("--use-ntvfs", help="Get the ACLs directly from the TDB or xattr used with the ntvfs file server", action="store_true"), + Option("--use-s3fs", help="Get the ACLs for use via the VFS layer used by the default s3fs file server", action="store_true"), + Option("--service", help="Name of the smb.conf service to use when getting the ACLs", type="string") + ] + + takes_args = ["file"] + + def run(self, file, use_ntvfs=False, use_s3fs=False, + as_sddl=False, xattr_backend=None, eadb_file=None, + credopts=None, sambaopts=None, versionopts=None, + service=None): + lp = sambaopts.get_loadparm() + try: + samdb = SamDB(session_info=system_session(), + lp=lp) + except Exception, e: + raise CommandError("Unable to open samdb:", e) + + if not use_ntvfs and not use_s3fs: + use_ntvfs = "smb" in lp.get("server services") + elif use_s3fs: + use_ntvfs = False + + + s3conf = s3param.get_context() + s3conf.load(lp.configfile) + # ensure we are using the right samba_dsdb passdb backend, no matter what + s3conf.set("passdb backend", "samba_dsdb:%s" % samdb.url) + + acl = getntacl(lp, file, xattr_backend, eadb_file, direct_db_access=use_ntvfs, service=service) + if as_sddl: + try: + domain_sid = security.dom_sid(samdb.domain_sid) + except: + raise CommandError("Unable to read domain SID from configuration files") + self.outf.write(acl.as_sddl(domain_sid)+"\n") + else: + self.outf.write(ndr_print(acl)) + + +class cmd_ntacl_sysvolreset(Command): + """Reset sysvol ACLs to defaults (including correct ACLs on GPOs).""" + synopsis = "%prog <file> [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_options = [ + Option("--use-ntvfs", help="Set the ACLs for use with the ntvfs file server", action="store_true"), + Option("--use-s3fs", help="Set the ACLs for use with the default s3fs file server", action="store_true") + ] + + def run(self, use_ntvfs=False, use_s3fs=False, + credopts=None, sambaopts=None, versionopts=None): + lp = sambaopts.get_loadparm() + path = lp.private_path("secrets.ldb") + creds = credopts.get_credentials(lp) + creds.set_kerberos_state(DONT_USE_KERBEROS) + logger = self.get_logger() + + netlogon = lp.get("path", "netlogon") + sysvol = lp.get("path", "sysvol") + try: + samdb = SamDB(session_info=system_session(), + lp=lp) + except Exception, e: + raise CommandError("Unable to open samdb:", e) + + if not use_ntvfs and not use_s3fs: + use_ntvfs = "smb" in lp.get("server services") + elif use_s3fs: + use_ntvfs = False + + domain_sid = security.dom_sid(samdb.domain_sid) + + s3conf = s3param.get_context() + s3conf.load(lp.configfile) + # ensure we are using the right samba_dsdb passdb backend, no matter what + s3conf.set("passdb backend", "samba_dsdb:%s" % samdb.url) + + LA_sid = security.dom_sid(str(domain_sid) + +"-"+str(security.DOMAIN_RID_ADMINISTRATOR)) + BA_sid = security.dom_sid(security.SID_BUILTIN_ADMINISTRATORS) + + s4_passdb = passdb.PDB(s3conf.get("passdb backend")) + + # These assertions correct for current plugin_s4_dc selftest + # configuration. When other environments have a broad range of + # groups mapped via passdb, we can relax some of these checks + (LA_uid,LA_type) = s4_passdb.sid_to_id(LA_sid) + if (LA_type != idmap.ID_TYPE_UID and LA_type != idmap.ID_TYPE_BOTH): + raise CommandError("SID %s is not mapped to a UID" % LA_sid) + (BA_gid,BA_type) = s4_passdb.sid_to_id(BA_sid) + if (BA_type != idmap.ID_TYPE_GID and BA_type != idmap.ID_TYPE_BOTH): + raise CommandError("SID %s is not mapped to a GID" % BA_sid) + + if use_ntvfs: + logger.warning("Please note that POSIX permissions have NOT been changed, only the stored NT ACL") + + provision.setsysvolacl(samdb, netlogon, sysvol, + LA_uid, BA_gid, domain_sid, + lp.get("realm").lower(), samdb.domain_dn(), + lp, use_ntvfs=use_ntvfs) + +class cmd_ntacl_sysvolcheck(Command): + """Check sysvol ACLs match defaults (including correct ACLs on GPOs).""" + synopsis = "%prog <file> [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + def run(self, credopts=None, sambaopts=None, versionopts=None): + lp = sambaopts.get_loadparm() + path = lp.private_path("secrets.ldb") + creds = credopts.get_credentials(lp) + creds.set_kerberos_state(DONT_USE_KERBEROS) + logger = self.get_logger() + + netlogon = lp.get("path", "netlogon") + sysvol = lp.get("path", "sysvol") + try: + samdb = SamDB(session_info=system_session(), lp=lp) + except Exception, e: + raise CommandError("Unable to open samdb:", e) + + domain_sid = security.dom_sid(samdb.domain_sid) + + provision.checksysvolacl(samdb, netlogon, sysvol, + domain_sid, + lp.get("realm").lower(), samdb.domain_dn(), + lp) + + +class cmd_ntacl(SuperCommand): + """NT ACLs manipulation.""" + + subcommands = {} + subcommands["set"] = cmd_ntacl_set() + subcommands["get"] = cmd_ntacl_get() + subcommands["sysvolreset"] = cmd_ntacl_sysvolreset() + subcommands["sysvolcheck"] = cmd_ntacl_sysvolcheck() + diff --git a/python/samba/netcmd/processes.py b/python/samba/netcmd/processes.py new file mode 100644 index 00000000000..b25a2e453ec --- /dev/null +++ b/python/samba/netcmd/processes.py @@ -0,0 +1,78 @@ +# Unix SMB/CIFS implementation. +# List processes (to aid debugging on systems without setproctitle) +# Copyright (C) 2010-2011 Jelmer Vernooij <jelmer@samba.org> +# +# 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/>. +# +# Testbed for loadparm.c/params.c +# +# This module simply loads a specified configuration file and +# if successful, dumps it's contents to stdout. Note that the +# operation is performed with DEBUGLEVEL at 3. +# +# Useful for a quick 'syntax check' of a configuration file. +# + +import os +import sys + +import samba +import samba.getopt as options +from samba.netcmd import Command, CommandError, Option +from samba.messaging import Messaging + +class cmd_processes(Command): + """List processes (to aid debugging on systems without setproctitle).""" + + synopsis = "%prog [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions + } + + takes_options = [ + Option("--name", type=str, + help="Return only processes associated with one particular name"), + Option("--pid", type=int, + help="Return only names assoicated with one particular PID"), + ] + + takes_args = [] + + def run(self, sambaopts, versionopts, section_name=None, + name=None, pid=None): + + lp = sambaopts.get_loadparm() + logger = self.get_logger("processes") + + msg_ctx = Messaging() + + if name is not None: + ids = msg_ctx.irpc_servers_byname(name) + for server_id in ids: + self.outf.write("%d\n" % server_id.pid) + elif pid is not None: + names = msg_ctx.irpc_all_servers() + for name in names: + for server_id in name.ids: + if server_id.pid == int(pid): + self.outf.write("%s\n" % name.name) + else: + names = msg_ctx.irpc_all_servers() + self.outf.write(" Service: PID \n") + self.outf.write("-----------------------------\n") + for name in names: + for server_id in name.ids: + self.outf.write("%-16s %6d\n" % (name.name, server_id.pid)) diff --git a/python/samba/netcmd/rodc.py b/python/samba/netcmd/rodc.py new file mode 100644 index 00000000000..2dc6112a302 --- /dev/null +++ b/python/samba/netcmd/rodc.py @@ -0,0 +1,108 @@ +# rodc related commands +# +# Copyright Andrew Tridgell 2010 +# +# 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/>. +# + +from samba.netcmd import Command, CommandError, Option, SuperCommand +import samba.getopt as options +from samba.samdb import SamDB +from samba.auth import system_session +import ldb +from samba.dcerpc import misc, drsuapi +from samba.drs_utils import drs_Replicate + + +class cmd_rodc_preload(Command): + """Preload one account for an RODC.""" + + synopsis = "%prog (<SID>|<DN>|<accountname>) [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("--server", help="DC to use", type=str), + ] + + takes_args = ["account"] + + def get_dn(self, samdb, account): + '''work out what DN they meant''' + + # we accept the account in SID, accountname or DN form + if account[0:2] == 'S-': + res = samdb.search(base="<SID=%s>" % account, + expression="objectclass=user", + scope=ldb.SCOPE_BASE, attrs=[]) + elif account.find('=') >= 0: + res = samdb.search(base=account, + expression="objectclass=user", + scope=ldb.SCOPE_BASE, attrs=[]) + else: + res = samdb.search(expression="(&(samAccountName=%s)(objectclass=user))" % ldb.binary_encode(account), + scope=ldb.SCOPE_SUBTREE, attrs=[]) + if len(res) != 1: + raise Exception("Failed to find account '%s'" % account) + return str(res[0]["dn"]) + + + def run(self, account, sambaopts=None, + credopts=None, versionopts=None, server=None): + + if server is None: + raise Exception("You must supply a server") + + lp = sambaopts.get_loadparm() + + creds = credopts.get_credentials(lp, fallback_machine=True) + + # connect to the remote and local SAMs + samdb = SamDB(url="ldap://%s" % server, + session_info=system_session(), + credentials=creds, lp=lp) + + local_samdb = SamDB(url=None, session_info=system_session(), + credentials=creds, lp=lp) + + # work out the source and destination GUIDs + dc_ntds_dn = samdb.get_dsServiceName() + res = samdb.search(base=dc_ntds_dn, scope=ldb.SCOPE_BASE, attrs=["invocationId"]) + source_dsa_invocation_id = misc.GUID(local_samdb.schema_format_value("objectGUID", res[0]["invocationId"][0])) + + dn = self.get_dn(samdb, account) + self.outf.write("Replicating DN %s\n" % dn) + + destination_dsa_guid = misc.GUID(local_samdb.get_ntds_GUID()) + + local_samdb.transaction_start() + repl = drs_Replicate("ncacn_ip_tcp:%s[seal,print]" % server, lp, creds, local_samdb) + try: + repl.replicate(dn, source_dsa_invocation_id, destination_dsa_guid, + exop=drsuapi.DRSUAPI_EXOP_REPL_SECRET, rodc=True) + except Exception, e: + raise CommandError("Error replicating DN %s" % dn, e) + local_samdb.transaction_commit() + + + +class cmd_rodc(SuperCommand): + """Read-Only Domain Controller (RODC) management.""" + + subcommands = {} + subcommands["preload"] = cmd_rodc_preload() diff --git a/python/samba/netcmd/sites.py b/python/samba/netcmd/sites.py new file mode 100644 index 00000000000..09df55ec9c5 --- /dev/null +++ b/python/samba/netcmd/sites.py @@ -0,0 +1,105 @@ +# sites management +# +# Copyright Matthieu Patou <mat@matws.net> 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 os +from samba import sites +from samba.samdb import SamDB +import samba.getopt as options +from samba.auth import system_session +from samba.netcmd import ( + Command, + CommandError, + SuperCommand + ) + + +class cmd_sites_create(Command): + """Create a new site.""" + + synopsis = "%prog <site> [options]" + + takes_args = ["sitename"] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + def run(self, sitename, sambaopts=None, credopts=None, versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + url = lp.private_path("sam.ldb") + + if not os.path.exists(url): + raise CommandError("secret database not found at %s " % url) + samdb = SamDB(url=url, session_info=system_session(), + credentials=creds, lp=lp) + + samdb.transaction_start() + try: + ok = sites.create_site(samdb, samdb.get_config_basedn(), sitename) + samdb.transaction_commit() + except sites.SiteAlreadyExistsException, e: + samdb.transaction_cancel() + raise CommandError("Error while creating site %s, error: %s" % (sitename, str(e))) + + self.outf.write("Site %s created !\n" % sitename) + +class cmd_sites_delete(Command): + """Delete an existing site.""" + + synopsis = "%prog <site> [options]" + + takes_args = ["sitename"] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + def run(self, sitename, sambaopts=None, credopts=None, versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + url = lp.private_path("sam.ldb") + + if not os.path.exists(url): + raise CommandError("secret database not found at %s " % url) + samdb = SamDB(url=url, session_info=system_session(), + credentials=creds, lp=lp) + + samdb.transaction_start() + try: + ok = sites.delete_site(samdb, samdb.get_config_basedn(), sitename) + samdb.transaction_commit() + except sites.SiteException, e: + samdb.transaction_cancel() + raise CommandError( + "Error while removing site %s, error: %s" % (sitename, str(e))) + + self.outf.write("Site %s removed!\n" % sitename) + + + +class cmd_sites(SuperCommand): + """Sites management.""" + + subcommands = {} + subcommands["create"] = cmd_sites_create() + subcommands["remove"] = cmd_sites_delete() diff --git a/python/samba/netcmd/spn.py b/python/samba/netcmd/spn.py new file mode 100644 index 00000000000..03d072ec9b6 --- /dev/null +++ b/python/samba/netcmd/spn.py @@ -0,0 +1,205 @@ +# spn management +# +# Copyright Matthieu Patou mat@samba.org 2010 +# +# 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 samba.getopt as options +import ldb +from samba import provision +from samba.samdb import SamDB +from samba.auth import system_session +from samba.netcmd.common import _get_user_realm_domain +from samba.netcmd import ( + Command, + CommandError, + SuperCommand, + Option + ) + + +class cmd_spn_list(Command): + """List spns of a given user.""" + + synopsis = "%prog <user> [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_args = ["user"] + + def run(self, user, credopts=None, sambaopts=None, versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + paths = provision.provision_paths_from_lp(lp, lp.get("realm")) + sam = SamDB(paths.samdb, session_info=system_session(), + credentials=creds, lp=lp) + # TODO once I understand how, use the domain info to naildown + # to the correct domain + (cleaneduser, realm, domain) = _get_user_realm_domain(user) + self.outf.write(cleaneduser+"\n") + res = sam.search( + expression="samaccountname=%s" % ldb.binary_encode(cleaneduser), + scope=ldb.SCOPE_SUBTREE, attrs=["servicePrincipalName"]) + if len(res) >0: + spns = res[0].get("servicePrincipalName") + found = False + flag = ldb.FLAG_MOD_ADD + if spns is not None: + self.outf.write( + "User %s has the following servicePrincipalName: \n" % + res[0].dn) + for e in spns: + self.outf.write("\t %s\n" % e) + else: + self.outf.write("User %s has no servicePrincipalName" % + res[0].dn) + else: + raise CommandError("User %s not found" % user) + + +class cmd_spn_add(Command): + """Create a new spn.""" + + synopsis = "%prog <name> <user> [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + takes_options = [ + Option("--force", help="Force the addition of the spn" + " even it exists already", action="store_true"), + ] + takes_args = ["name", "user"] + + def run(self, name, user, force=False, credopts=None, sambaopts=None, + versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + paths = provision.provision_paths_from_lp(lp, lp.get("realm")) + sam = SamDB(paths.samdb, session_info=system_session(), + credentials=creds, lp=lp) + res = sam.search( + expression="servicePrincipalName=%s" % ldb.binary_encode(name), + scope=ldb.SCOPE_SUBTREE) + if len(res) != 0 and not force: + raise CommandError("Service principal %s already" + " affected to another user" % name) + + (cleaneduser, realm, domain) = _get_user_realm_domain(user) + res = sam.search( + expression="samaccountname=%s" % ldb.binary_encode(cleaneduser), + scope=ldb.SCOPE_SUBTREE, attrs=["servicePrincipalName"]) + if len(res) >0: + res[0].dn + msg = ldb.Message() + spns = res[0].get("servicePrincipalName") + tab = [] + found = False + flag = ldb.FLAG_MOD_ADD + if spns is not None: + for e in spns: + if str(e) == name: + found = True + tab.append(str(e)) + flag = ldb.FLAG_MOD_REPLACE + tab.append(name) + msg.dn = res[0].dn + msg["servicePrincipalName"] = ldb.MessageElement(tab, flag, + "servicePrincipalName") + if not found: + sam.modify(msg) + else: + raise CommandError("Service principal %s already" + " affected to %s" % (name, user)) + else: + raise CommandError("User %s not found" % user) + + +class cmd_spn_delete(Command): + """Delete a spn.""" + + synopsis = "%prog <name> [user] [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_args = ["name", "user?"] + + def run(self, name, user=None, credopts=None, sambaopts=None, + versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + paths = provision.provision_paths_from_lp(lp, lp.get("realm")) + sam = SamDB(paths.samdb, session_info=system_session(), + credentials=creds, lp=lp) + res = sam.search( + expression="servicePrincipalName=%s" % ldb.binary_encode(name), + scope=ldb.SCOPE_SUBTREE, + attrs=["servicePrincipalName", "samAccountName"]) + if len(res) >0: + result = None + if user is not None: + (cleaneduser, realm, domain) = _get_user_realm_domain(user) + for elem in res: + if str(elem["samAccountName"]).lower() == cleaneduser: + result = elem + if result is None: + raise CommandError("Unable to find user %s with" + " spn %s" % (user, name)) + else: + if len(res) != 1: + listUser = "" + for r in res: + listUser = "%s\n%s" % (listUser, str(r.dn)) + raise CommandError("More than one user has the spn %s " + "and no specific user was specified, list of users" + " with this spn:%s" % (name, listUser)) + else: + result=res[0] + + + msg = ldb.Message() + spns = result.get("servicePrincipalName") + tab = [] + if spns is not None: + for e in spns: + if str(e) != name: + tab.append(str(e)) + flag = ldb.FLAG_MOD_REPLACE + msg.dn = result.dn + msg["servicePrincipalName"] = ldb.MessageElement(tab, flag, + "servicePrincipalName") + sam.modify(msg) + else: + raise CommandError("Service principal %s not affected" % name) + + +class cmd_spn(SuperCommand): + """Service Principal Name (SPN) management.""" + + subcommands = {} + subcommands["add"] = cmd_spn_add() + subcommands["list"] = cmd_spn_list() + subcommands["delete"] = cmd_spn_delete() + diff --git a/python/samba/netcmd/testparm.py b/python/samba/netcmd/testparm.py new file mode 100644 index 00000000000..92514694218 --- /dev/null +++ b/python/samba/netcmd/testparm.py @@ -0,0 +1,209 @@ +# Unix SMB/CIFS implementation. +# Test validity of smb.conf +# Copyright (C) 2010-2011 Jelmer Vernooij <jelmer@samba.org> +# +# Based on the original in C: +# Copyright (C) Karl Auer 1993, 1994-1998 +# Extensively modified by Andrew Tridgell, 1995 +# Converted to popt by Jelmer Vernooij (jelmer@nl.linux.org), 2002 +# Updated for Samba4 by Andrew Bartlett <abartlet@samba.org> 2006 +# +# 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/>. +# +# Testbed for loadparm.c/params.c +# +# This module simply loads a specified configuration file and +# if successful, dumps it's contents to stdout. Note that the +# operation is performed with DEBUGLEVEL at 3. +# +# Useful for a quick 'syntax check' of a configuration file. +# + +import os +import sys + +import samba +import samba.getopt as options +from samba.netcmd import Command, CommandError, Option + +class cmd_testparm(Command): + """Syntax check the configuration file.""" + + synopsis = "%prog [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions + } + + takes_options = [ + Option("--section-name", type=str, + help="Limit testparm to a named section"), + Option("--parameter-name", type=str, + help="Limit testparm to a named parameter"), + Option("--client-name", type=str, + help="Client DNS name for 'hosts allow' checking " + "(should match reverse lookup)"), + Option("--client-ip", type=str, + help="Client IP address for 'hosts allow' checking"), + Option("--suppress-prompt", action="store_true", default=False, + help="Suppress prompt for enter"), + Option("-v", "--verbose", action="store_true", + default=False, help="Show default options too"), + # We need support for smb.conf macros before this will work again + Option("--server", type=str, help="Set %L macro to servername"), + # These are harder to do with the new code structure + Option("--show-all-parameters", action="store_true", default=False, + help="Show the parameters, type, possible values") + ] + + takes_args = [] + + def run(self, sambaopts, versionopts, section_name=None, + parameter_name=None, client_ip=None, client_name=None, + verbose=False, suppress_prompt=None, show_all_parameters=False, + server=None): + if server: + raise NotImplementedError("--server not yet implemented") + if show_all_parameters: + raise NotImplementedError("--show-all-parameters not yet implemented") + if client_name is not None and client_ip is None: + raise CommandError("Both a DNS name and an IP address are " + "required for the host access check") + + try: + lp = sambaopts.get_loadparm() + except RuntimeError, err: + raise CommandError(err) + + # We need this to force the output + samba.set_debug_level(2) + + logger = self.get_logger("testparm") + + logger.info("Loaded smb config files from %s", lp.configfile) + logger.info("Loaded services file OK.") + + valid = self.do_global_checks(lp, logger) + valid = valid and self.do_share_checks(lp, logger) + if client_name is not None and client_ip is not None: + self.check_client_access(lp, logger, client_name, client_ip) + else: + if section_name is not None or parameter_name is not None: + if parameter_name is None: + lp[section_name].dump(sys.stdout, lp.default_service, + verbose) + else: + self.outf.write(lp.get(parameter_name, section_name)+"\n") + else: + if not suppress_prompt: + self.outf.write("Press enter to see a dump of your service definitions\n") + sys.stdin.readline() + lp.dump(sys.stdout, verbose) + if valid: + return + else: + raise CommandError("Invalid smb.conf") + + def do_global_checks(self, lp, logger): + valid = True + + netbios_name = lp.get("netbios name") + if not samba.valid_netbios_name(netbios_name): + logger.error("netbios name %s is not a valid netbios name", + netbios_name) + valid = False + + workgroup = lp.get("workgroup") + if not samba.valid_netbios_name(workgroup): + logger.error("workgroup name %s is not a valid netbios name", + workgroup) + valid = False + + lockdir = lp.get("lockdir") + + if not os.path.isdir(lockdir): + logger.error("lock directory %s does not exist", lockdir) + valid = False + + piddir = lp.get("pid directory") + + if not os.path.isdir(piddir): + logger.error("pid directory %s does not exist", piddir) + valid = False + + winbind_separator = lp.get("winbind separator") + + if len(winbind_separator) != 1: + logger.error("the 'winbind separator' parameter must be a single " + "character.") + valid = False + + if winbind_separator == '+': + logger.error( + "'winbind separator = +' might cause problems with group " + "membership.") + valid = False + + return valid + + def allow_access(self, deny_list, allow_list, cname, caddr): + raise NotImplementedError(self.allow_access) + + def do_share_checks(self, lp, logger): + valid = True + for s in lp.services(): + if len(s) > 12: + logger.warning( + "You have some share names that are longer than 12 " + "characters. These may not be accessible to some older " + "clients. (Eg. Windows9x, WindowsMe, and not listed in " + "smbclient in Samba 3.0.)") + break + + for s in lp.services(): + deny_list = lp.get("hosts deny", s) + allow_list = lp.get("hosts allow", s) + if deny_list: + for entry in deny_list: + if "*" in entry or "?" in entry: + logger.error("Invalid character (* or ?) in hosts deny " + "list (%s) for service %s.", entry, s) + valid = False + + if allow_list: + for entry in allow_list: + if "*" in entry or "?" in entry: + logger.error("Invalid character (* or ?) in hosts allow " + "list (%s) for service %s.", entry, s) + valid = False + return valid + + def check_client_access(self, lp, logger, cname, caddr): + # this is totally ugly, a real `quick' hack + for s in lp.services(): + if (self.allow_access(lp.get("hosts deny"), lp.get("hosts allow"), cname, + caddr) and + self.allow_access(lp.get("hosts deny", s), lp.get("hosts allow", s), + cname, caddr)): + logger.info("Allow connection from %s (%s) to %s", cname, caddr, s) + else: + logger.info("Deny connection from %s (%s) to %s", cname, caddr, s) + +## FIXME: We need support for smb.conf macros before this will work again +## +## if (new_local_machine) { +## set_local_machine_name(new_local_machine, True) +## } +# diff --git a/python/samba/netcmd/time.py b/python/samba/netcmd/time.py new file mode 100644 index 00000000000..694b6adda9b --- /dev/null +++ b/python/samba/netcmd/time.py @@ -0,0 +1,59 @@ +# time +# +# Copyright Jelmer Vernooij 2010 <jelmer@samba.org> +# +# 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 samba.getopt as options +import common +from samba.net import Net + +from samba.netcmd import ( + Command, + ) + +class cmd_time(Command): + """Retrieve the time on a server. + +This command returns the date and time of the Active Directory server specified on the command. The server name specified may be the local server or a remote server. If the servername is not specified, the command returns the time and date of the local AD server. + +Example1: +samba-tool time samdom.example.com + +Example1 returns the date and time of the server samdom.example.com. + +Example2: +samba-tool time + +Example2 return the date and time of the local server. +""" + synopsis = "%prog [server-name] [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_args = ["server_name?"] + + def run(self, server_name=None, credopts=None, sambaopts=None, + versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + net = Net(creds, lp, server=credopts.ipaddress) + if server_name is None: + server_name = common.netcmd_dnsname(lp) + self.outf.write(net.time(server_name)+"\n") diff --git a/python/samba/netcmd/user.py b/python/samba/netcmd/user.py new file mode 100644 index 00000000000..b98ec344b2a --- /dev/null +++ b/python/samba/netcmd/user.py @@ -0,0 +1,605 @@ +# user management +# +# Copyright Jelmer Vernooij 2010 <jelmer@samba.org> +# Copyright Theresa Halloran 2011 <theresahalloran@gmail.com> +# +# 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 samba.getopt as options +import ldb +import pwd +from getpass import getpass +from samba.auth import system_session +from samba.samdb import SamDB +from samba import ( + dsdb, + gensec, + generate_random_password, + ) +from samba.net import Net + +from samba.netcmd import ( + Command, + CommandError, + SuperCommand, + Option, + ) + + +class cmd_user_create(Command): + """Create a new user. + +This command creates a new user account in the Active Directory domain. The username specified on the command is the sAMaccountName. + +User accounts may represent physical entities, such as people or may be used as service accounts for applications. User accounts are also referred to as security principals and are assigned a security identifier (SID). + +A user account enables a user to logon to a computer and domain with an identity that can be authenticated. To maximize security, each user should have their own unique user account and password. A user's access to domain resources is based on permissions assigned to the user account. + +Unix (RFC2307) attributes may be added to the user account. Attributes taken from NSS are obtained on the local machine. Explicitly given values override values obtained from NSS. Configure 'idmap_ldb:use rfc2307 = Yes' to use these attributes for UID/GID mapping. + +The command may be run from the root userid or another authorized userid. The -H or --URL= option can be used to execute the command against a remote server. + +Example1: +samba-tool user add User1 passw0rd --given-name=John --surname=Smith --must-change-at-next-login -H ldap://samba.samdom.example.com -Uadministrator%passw1rd + +Example1 shows how to create a new user in the domain against a remote LDAP server. The -H parameter is used to specify the remote target server. The -U option is used to pass the userid and password authorized to issue the command remotely. + +Example2: +sudo samba-tool user add User2 passw2rd --given-name=Jane --surname=Doe --must-change-at-next-login + +Example2 shows how to create a new user in the domain against the local server. sudo is used so a user may run the command as root. In this example, after User2 is created, he/she will be forced to change their password when they logon. + +Example3: +samba-tool user add User3 passw3rd --userou=OrgUnit + +Example3 shows how to create a new user in the OrgUnit organizational unit. + +Example4: +samba-tool user create User4 passw4rd --rfc2307-from-nss --gecos 'some text' + +Example4 shows how to create a new user with Unix UID, GID and login-shell set from the local NSS and GECOS set to 'some text'. + +""" + synopsis = "%prog <username> [<password>] [options]" + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H"), + Option("--must-change-at-next-login", + help="Force password to be changed on next login", + action="store_true"), + Option("--random-password", + help="Generate random password", + action="store_true"), + Option("--use-username-as-cn", + help="Force use of username as user's CN", + action="store_true"), + Option("--userou", + help="Alternative location (without domainDN counterpart) to default CN=Users in which new user object will be created", + type=str), + Option("--surname", help="User's surname", type=str), + Option("--given-name", help="User's given name", type=str), + Option("--initials", help="User's initials", type=str), + Option("--profile-path", help="User's profile path", type=str), + Option("--script-path", help="User's logon script path", type=str), + Option("--home-drive", help="User's home drive letter", type=str), + Option("--home-directory", help="User's home directory path", type=str), + Option("--job-title", help="User's job title", type=str), + Option("--department", help="User's department", type=str), + Option("--company", help="User's company", type=str), + Option("--description", help="User's description", type=str), + Option("--mail-address", help="User's email address", type=str), + Option("--internet-address", help="User's home page", type=str), + Option("--telephone-number", help="User's phone number", type=str), + Option("--physical-delivery-office", help="User's office location", type=str), + Option("--rfc2307-from-nss", + help="Copy Unix user attributes from NSS (will be overridden by explicit UID/GID/GECOS/shell)", + action="store_true"), + Option("--uid", help="User's Unix/RFC2307 username", type=str), + Option("--uid-number", help="User's Unix/RFC2307 numeric UID", type=int), + Option("--gid-number", help="User's Unix/RFC2307 primary GID number", type=int), + Option("--gecos", help="User's Unix/RFC2307 GECOS field", type=str), + Option("--login-shell", help="User's Unix/RFC2307 login shell", type=str), + ] + + takes_args = ["username", "password?"] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + def run(self, username, password=None, credopts=None, sambaopts=None, + versionopts=None, H=None, must_change_at_next_login=False, + random_password=False, use_username_as_cn=False, userou=None, + surname=None, given_name=None, initials=None, profile_path=None, + script_path=None, home_drive=None, home_directory=None, + job_title=None, department=None, company=None, description=None, + mail_address=None, internet_address=None, telephone_number=None, + physical_delivery_office=None, rfc2307_from_nss=False, + uid=None, uid_number=None, gid_number=None, gecos=None, login_shell=None): + + if random_password: + password = generate_random_password(128, 255) + + while True: + if password is not None and password is not '': + break + password = getpass("New Password: ") + passwordverify = getpass("Retype Password: ") + if not password == passwordverify: + password = None + self.outf.write("Sorry, passwords do not match.\n") + + if rfc2307_from_nss: + pwent = pwd.getpwnam(username) + if uid is None: + uid = username + if uid_number is None: + uid_number = pwent[2] + if gid_number is None: + gid_number = pwent[3] + if gecos is None: + gecos = pwent[4] + if login_shell is None: + login_shell = pwent[6] + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + + if uid_number or gid_number: + if not lp.get("idmap_ldb:use rfc2307"): + self.outf.write("You are setting a Unix/RFC2307 UID or GID. You may want to set 'idmap_ldb:use rfc2307 = Yes' to use those attributes for XID/SID-mapping.\n") + + try: + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + samdb.newuser(username, password, force_password_change_at_next_login_req=must_change_at_next_login, + useusernameascn=use_username_as_cn, userou=userou, surname=surname, givenname=given_name, initials=initials, + profilepath=profile_path, homedrive=home_drive, scriptpath=script_path, homedirectory=home_directory, + jobtitle=job_title, department=department, company=company, description=description, + mailaddress=mail_address, internetaddress=internet_address, + telephonenumber=telephone_number, physicaldeliveryoffice=physical_delivery_office, + uid=uid, uidnumber=uid_number, gidnumber=gid_number, gecos=gecos, loginshell=login_shell) + except Exception, e: + raise CommandError("Failed to add user '%s': " % username, e) + + self.outf.write("User '%s' created successfully\n" % username) + + +class cmd_user_add(cmd_user_create): + __doc__ = cmd_user_create.__doc__ + # take this print out after the add subcommand is removed. + # the add subcommand is deprecated but left in for now to allow people to + # migrate to create + + def run(self, *args, **kwargs): + self.err.write( + "Note: samba-tool user add is deprecated. " + "Please use samba-tool user create for the same function.\n") + return super(self, cmd_user_add).run(*args, **kwargs) + + +class cmd_user_delete(Command): + """Delete a user. + +This command deletes a user account from the Active Directory domain. The username specified on the command is the sAMAccountName. + +Once the account is deleted, all permissions and memberships associated with that account are deleted. If a new user account is added with the same name as a previously deleted account name, the new user does not have the previous permissions. The new account user will be assigned a new security identifier (SID) and permissions and memberships will have to be added. + +The command may be run from the root userid or another authorized userid. The -H or --URL= option can be used to execute the command against a remote server. + +Example1: +samba-tool user delete User1 -H ldap://samba.samdom.example.com --username=administrator --password=passw1rd + +Example1 shows how to delete a user in the domain against a remote LDAP server. The -H parameter is used to specify the remote target server. The --username= and --password= options are used to pass the username and password of a user that exists on the remote server and is authorized to issue the command on that server. + +Example2: +sudo samba-tool user delete User2 + +Example2 shows how to delete a user in the domain against the local server. sudo is used so a user may run the command as root. + +""" + synopsis = "%prog <username> [options]" + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H"), + ] + + takes_args = ["username"] + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + def run(self, username, credopts=None, sambaopts=None, versionopts=None, + H=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + + try: + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + samdb.deleteuser(username) + except Exception, e: + raise CommandError('Failed to remove user "%s"' % username, e) + self.outf.write("Deleted user %s\n" % username) + + +class cmd_user_list(Command): + """List all users.""" + + synopsis = "%prog [options]" + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H"), + ] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + def run(self, sambaopts=None, credopts=None, versionopts=None, H=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + + domain_dn = samdb.domain_dn() + res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE, + expression=("(&(objectClass=user)(userAccountControl:%s:=%u))" + % (ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT)), + attrs=["samaccountname"]) + if (len(res) == 0): + return + + for msg in res: + self.outf.write("%s\n" % msg.get("samaccountname", idx=0)) + + +class cmd_user_enable(Command): + """Enable an user. + +This command enables a user account for logon to an Active Directory domain. The username specified on the command is the sAMAccountName. The username may also be specified using the --filter option. + +There are many reasons why an account may become disabled. These include: +- If a user exceeds the account policy for logon attempts +- If an administrator disables the account +- If the account expires + +The samba-tool user enable command allows an administrator to enable an account which has become disabled. + +Additionally, the enable function allows an administrator to have a set of created user accounts defined and setup with default permissions that can be easily enabled for use. + +The command may be run from the root userid or another authorized userid. The -H or --URL= option can be used to execute the command against a remote server. + +Example1: +samba-tool user enable Testuser1 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd + +Example1 shows how to enable a user in the domain against a remote LDAP server. The --URL parameter is used to specify the remote target server. The --username= and --password= options are used to pass the username and password of a user that exists on the remote server and is authorized to update that server. + +Example2: +su samba-tool user enable Testuser2 + +Example2 shows how to enable user Testuser2 for use in the domain on the local server. sudo is used so a user may run the command as root. + +Example3: +samba-tool user enable --filter=samaccountname=Testuser3 + +Example3 shows how to enable a user in the domain against a local LDAP server. It uses the --filter=samaccountname to specify the username. + +""" + synopsis = "%prog (<username>|--filter <filter>) [options]" + + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H"), + Option("--filter", help="LDAP Filter to set password on", type=str), + ] + + takes_args = ["username?"] + + def run(self, username=None, sambaopts=None, credopts=None, + versionopts=None, filter=None, H=None): + if username is None and filter is None: + raise CommandError("Either the username or '--filter' must be specified!") + + if filter is None: + filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username)) + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + try: + samdb.enable_account(filter) + except Exception, msg: + raise CommandError("Failed to enable user '%s': %s" % (username or filter, msg)) + self.outf.write("Enabled user '%s'\n" % (username or filter)) + + +class cmd_user_disable(Command): + """Disable an user.""" + + synopsis = "%prog (<username>|--filter <filter>) [options]" + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H"), + Option("--filter", help="LDAP Filter to set password on", type=str), + ] + + takes_args = ["username?"] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + def run(self, username=None, sambaopts=None, credopts=None, + versionopts=None, filter=None, H=None): + if username is None and filter is None: + raise CommandError("Either the username or '--filter' must be specified!") + + if filter is None: + filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username)) + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + try: + samdb.disable_account(filter) + except Exception, msg: + raise CommandError("Failed to disable user '%s': %s" % (username or filter, msg)) + + +class cmd_user_setexpiry(Command): + """Set the expiration of a user account. + +The user can either be specified by their sAMAccountName or using the --filter option. + +When a user account expires, it becomes disabled and the user is unable to logon. The administrator may issue the samba-tool user enable command to enable the account for logon. The permissions and memberships associated with the account are retained when the account is enabled. + +The command may be run from the root userid or another authorized userid. The -H or --URL= option can be used to execute the command on a remote server. + +Example1: +samba-tool user setexpiry User1 --days=20 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd + +Example1 shows how to set the expiration of an account in a remote LDAP server. The --URL parameter is used to specify the remote target server. The --username= and --password= options are used to pass the username and password of a user that exists on the remote server and is authorized to update that server. + +Example2: +su samba-tool user setexpiry User2 + +Example2 shows how to set the account expiration of user User2 so it will never expire. The user in this example resides on the local server. sudo is used so a user may run the command as root. + +Example3: +samba-tool user setexpiry --days=20 --filter=samaccountname=User3 + +Example3 shows how to set the account expiration date to end of day 20 days from the current day. The username or sAMAccountName is specified using the --filter= paramter and the username in this example is User3. + +Example4: +samba-tool user setexpiry --noexpiry User4 +Example4 shows how to set the account expiration so that it will never expire. The username and sAMAccountName in this example is User4. + +""" + synopsis = "%prog (<username>|--filter <filter>) [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H"), + Option("--filter", help="LDAP Filter to set password on", type=str), + Option("--days", help="Days to expiry", type=int, default=0), + Option("--noexpiry", help="Password does never expire", action="store_true", default=False), + ] + + takes_args = ["username?"] + + def run(self, username=None, sambaopts=None, credopts=None, + versionopts=None, H=None, filter=None, days=None, noexpiry=None): + if username is None and filter is None: + raise CommandError("Either the username or '--filter' must be specified!") + + if filter is None: + filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username)) + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + + try: + samdb.setexpiry(filter, days*24*3600, no_expiry_req=noexpiry) + except Exception, msg: + # FIXME: Catch more specific exception + raise CommandError("Failed to set expiry for user '%s': %s" % ( + username or filter, msg)) + if days: + self.outf.write("Expiry for user '%s' set to %u days.\n" % ( + username or filter, days)) + else: + self.outf.write("Expiry for user '%s' disabled.\n" % ( + username or filter)) + + +class cmd_user_password(Command): + """Change password for a user account (the one provided in authentication). +""" + + synopsis = "%prog [options]" + + takes_options = [ + Option("--newpassword", help="New password", type=str), + ] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + def run(self, credopts=None, sambaopts=None, versionopts=None, + newpassword=None): + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + + # get old password now, to get the password prompts in the right order + old_password = creds.get_password() + + net = Net(creds, lp, server=credopts.ipaddress) + + password = newpassword + while True: + if password is not None and password is not '': + break + password = getpass("New Password: ") + passwordverify = getpass("Retype Password: ") + if not password == passwordverify: + password = None + self.outf.write("Sorry, passwords do not match.\n") + + try: + net.change_password(password) + except Exception, msg: + # FIXME: catch more specific exception + raise CommandError("Failed to change password : %s" % msg) + self.outf.write("Changed password OK\n") + + +class cmd_user_setpassword(Command): + """Set or reset the password of a user account. + +This command sets or resets the logon password for a user account. The username specified on the command is the sAMAccountName. The username may also be specified using the --filter option. + +If the password is not specified on the command through the --newpassword parameter, the user is prompted for the password to be entered through the command line. + +It is good security practice for the administrator to use the --must-change-at-next-login option which requires that when the user logs on to the account for the first time following the password change, he/she must change the password. + +The command may be run from the root userid or another authorized userid. The -H or --URL= option can be used to execute the command against a remote server. + +Example1: +samba-tool user setpassword TestUser1 --newpassword=passw0rd --URL=ldap://samba.samdom.example.com -Uadministrator%passw1rd + +Example1 shows how to set the password of user TestUser1 on a remote LDAP server. The --URL parameter is used to specify the remote target server. The -U option is used to pass the username and password of a user that exists on the remote server and is authorized to update the server. + +Example2: +sudo samba-tool user setpassword TestUser2 --newpassword=passw0rd --must-change-at-next-login + +Example2 shows how an administrator would reset the TestUser2 user's password to passw0rd. The user is running under the root userid using the sudo command. In this example the user TestUser2 must change their password the next time they logon to the account. + +Example3: +samba-tool user setpassword --filter=samaccountname=TestUser3 --newpassword=passw0rd + +Example3 shows how an administrator would reset TestUser3 user's password to passw0rd using the --filter= option to specify the username. + +""" + synopsis = "%prog (<username>|--filter <filter>) [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H"), + Option("--filter", help="LDAP Filter to set password on", type=str), + Option("--newpassword", help="Set password", type=str), + Option("--must-change-at-next-login", + help="Force password to be changed on next login", + action="store_true"), + Option("--random-password", + help="Generate random password", + action="store_true"), + ] + + takes_args = ["username?"] + + def run(self, username=None, filter=None, credopts=None, sambaopts=None, + versionopts=None, H=None, newpassword=None, + must_change_at_next_login=False, random_password=False): + if filter is None and username is None: + raise CommandError("Either the username or '--filter' must be specified!") + + if random_password: + password = generate_random_password(128, 255) + else: + password = newpassword + + while 1: + if password is not None and password is not '': + break + password = getpass("New Password: ") + + if filter is None: + filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username)) + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + + creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL) + + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + + try: + samdb.setpassword(filter, password, + force_change_at_next_login=must_change_at_next_login, + username=username) + except Exception, msg: + # FIXME: catch more specific exception + raise CommandError("Failed to set password for user '%s': %s" % (username or filter, msg)) + self.outf.write("Changed password OK\n") + + +class cmd_user(SuperCommand): + """User management.""" + + subcommands = {} + subcommands["add"] = cmd_user_create() + subcommands["create"] = cmd_user_create() + subcommands["delete"] = cmd_user_delete() + subcommands["disable"] = cmd_user_disable() + subcommands["enable"] = cmd_user_enable() + subcommands["list"] = cmd_user_list() + subcommands["setexpiry"] = cmd_user_setexpiry() + subcommands["password"] = cmd_user_password() + subcommands["setpassword"] = cmd_user_setpassword() diff --git a/python/samba/netcmd/vampire.py b/python/samba/netcmd/vampire.py new file mode 100644 index 00000000000..b12222e79e6 --- /dev/null +++ b/python/samba/netcmd/vampire.py @@ -0,0 +1,55 @@ +# Vampire +# +# Copyright Jelmer Vernooij 2010 <jelmer@samba.org> +# +# 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 samba.getopt as options + +from samba.net import Net + +from samba.netcmd import ( + Command, + Option, + SuperCommand, + CommandError + ) + + +class cmd_vampire(Command): + """Join and synchronise a remote AD domain to the local server.""" + synopsis = "%prog [options] <domain>" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_options = [ + Option("--target-dir", help="Target directory.", type=str), + Option("--force", help="force run", action='store_true', default=False), + ] + + takes_args = ["domain"] + + def run(self, domain, target_dir=None, credopts=None, sambaopts=None, versionopts=None, force=False): + if not force: + raise CommandError("samba-tool vampire is deprecated, please use samba-tool domain join. Use --force to override") + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + net = Net(creds, lp, server=credopts.ipaddress) + (domain_name, domain_sid) = net.vampire(domain=domain, target_dir=target_dir) + self.outf.write("Vampired domain %s (%s)\n" % (domain_name, domain_sid)) diff --git a/python/samba/ntacls.py b/python/samba/ntacls.py new file mode 100644 index 00000000000..53438d84bff --- /dev/null +++ b/python/samba/ntacls.py @@ -0,0 +1,240 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Matthieu Patou <mat@matws.net> 2009-2010 +# +# +# 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/>. +# + +"""NT Acls.""" + + +import os +import samba.xattr_native, samba.xattr_tdb, samba.posix_eadb +from samba.dcerpc import security, xattr, idmap +from samba.ndr import ndr_pack, ndr_unpack +from samba.samba3 import smbd + +class XattrBackendError(Exception): + """A generic xattr backend error.""" + + +def checkset_backend(lp, backend, eadbfile): + '''return the path to the eadb, or None''' + if backend is None: + xattr_tdb = lp.get("xattr_tdb:file") + if xattr_tdb is not None: + return (samba.xattr_tdb, lp.get("xattr_tdb:file")) + posix_eadb = lp.get("posix:eadb") + if posix_eadb is not None: + return (samba.posix_eadb, lp.get("posix:eadb")) + return (None, None) + elif backend == "native": + return (None, None) + elif backend == "eadb": + if eadbfile is not None: + return (samba.posix_eadb, eadbfile) + else: + return (samba.posix_eadb, os.path.abspath(os.path.join(lp.get("private dir"), "eadb.tdb"))) + elif backend == "tdb": + if eadbfile is not None: + return (samba.xattr_tdb, eadbfile) + else: + return (samba.xattr_tdb, os.path.abspath(os.path.join(lp.get("state dir"), "xattr.tdb"))) + else: + raise XattrBackendError("Invalid xattr backend choice %s"%backend) + + +def getntacl(lp, file, backend=None, eadbfile=None, direct_db_access=True, service=None): + if direct_db_access: + (backend_obj, dbname) = checkset_backend(lp, backend, eadbfile) + if dbname is not None: + try: + attribute = backend_obj.wrap_getxattr(dbname, file, + xattr.XATTR_NTACL_NAME) + except Exception: + # FIXME: Don't catch all exceptions, just those related to opening + # xattrdb + print "Fail to open %s" % dbname + attribute = samba.xattr_native.wrap_getxattr(file, + xattr.XATTR_NTACL_NAME) + else: + attribute = samba.xattr_native.wrap_getxattr(file, + xattr.XATTR_NTACL_NAME) + ntacl = ndr_unpack(xattr.NTACL, attribute) + if ntacl.version == 1: + return ntacl.info + elif ntacl.version == 2: + return ntacl.info.sd + elif ntacl.version == 3: + return ntacl.info.sd + elif ntacl.version == 4: + return ntacl.info.sd + else: + return smbd.get_nt_acl(file, security.SECINFO_OWNER | security.SECINFO_GROUP | security.SECINFO_DACL | security.SECINFO_SACL, service=service) + + +def setntacl(lp, file, sddl, domsid, backend=None, eadbfile=None, use_ntvfs=True, skip_invalid_chown=False, passdb=None, service=None): + assert(isinstance(domsid, str) or isinstance(domsid, security.dom_sid)) + if isinstance(domsid, str): + sid = security.dom_sid(domsid) + elif isinstance(domsid, security.dom_sid): + sid = domsid + domsid = str(sid) + + assert(isinstance(sddl, str) or isinstance(sddl, security.descriptor)) + if isinstance(sddl, str): + sd = security.descriptor.from_sddl(sddl, sid) + elif isinstance(sddl, security.descriptor): + sd = sddl + sddl = sd.as_sddl(sid) + + if not use_ntvfs and skip_invalid_chown: + # Check if the owner can be resolved as a UID + (owner_id, owner_type) = passdb.sid_to_id(sd.owner_sid) + if ((owner_type != idmap.ID_TYPE_UID) and (owner_type != idmap.ID_TYPE_BOTH)): + # Check if this particular owner SID was domain admins, + # because we special-case this as mapping to + # 'administrator' instead. + if sd.owner_sid == security.dom_sid("%s-%d" % (domsid, security.DOMAIN_RID_ADMINS)): + administrator = security.dom_sid("%s-%d" % (domsid, security.DOMAIN_RID_ADMINISTRATOR)) + (admin_id, admin_type) = passdb.sid_to_id(administrator) + + # Confirm we have a UID for administrator + if ((admin_type == idmap.ID_TYPE_UID) or (admin_type == idmap.ID_TYPE_BOTH)): + + # Set it, changing the owner to 'administrator' rather than domain admins + sd2 = sd + sd2.owner_sid = administrator + + smbd.set_nt_acl(file, security.SECINFO_OWNER |security.SECINFO_GROUP | security.SECINFO_DACL | security.SECINFO_SACL, sd2, service=service) + + # and then set an NTVFS ACL (which does not set the posix ACL) to pretend the owner really was set + use_ntvfs = True + else: + raise XattrBackendError("Unable to find UID for domain administrator %s, got id %d of type %d" % (administrator, admin_id, admin_type)) + else: + # For all other owning users, reset the owner to root + # and then set the ACL without changing the owner + # + # This won't work in test environments, as it tries a real (rather than xattr-based fake) chown + + os.chown(file, 0, 0) + smbd.set_nt_acl(file, security.SECINFO_GROUP | security.SECINFO_DACL | security.SECINFO_SACL, sd, service=service) + + if use_ntvfs: + (backend_obj, dbname) = checkset_backend(lp, backend, eadbfile) + ntacl = xattr.NTACL() + ntacl.version = 1 + ntacl.info = sd + if dbname is not None: + try: + backend_obj.wrap_setxattr(dbname, + file, xattr.XATTR_NTACL_NAME, ndr_pack(ntacl)) + except Exception: + # FIXME: Don't catch all exceptions, just those related to opening + # xattrdb + print "Fail to open %s" % dbname + samba.xattr_native.wrap_setxattr(file, xattr.XATTR_NTACL_NAME, + ndr_pack(ntacl)) + else: + samba.xattr_native.wrap_setxattr(file, xattr.XATTR_NTACL_NAME, + ndr_pack(ntacl)) + else: + smbd.set_nt_acl(file, security.SECINFO_OWNER | security.SECINFO_GROUP | security.SECINFO_DACL | security.SECINFO_SACL, sd, service=service) + + +def ldapmask2filemask(ldm): + """Takes the access mask of a DS ACE and transform them in a File ACE mask. + """ + RIGHT_DS_CREATE_CHILD = 0x00000001 + RIGHT_DS_DELETE_CHILD = 0x00000002 + RIGHT_DS_LIST_CONTENTS = 0x00000004 + ACTRL_DS_SELF = 0x00000008 + RIGHT_DS_READ_PROPERTY = 0x00000010 + RIGHT_DS_WRITE_PROPERTY = 0x00000020 + RIGHT_DS_DELETE_TREE = 0x00000040 + RIGHT_DS_LIST_OBJECT = 0x00000080 + RIGHT_DS_CONTROL_ACCESS = 0x00000100 + FILE_READ_DATA = 0x0001 + FILE_LIST_DIRECTORY = 0x0001 + FILE_WRITE_DATA = 0x0002 + FILE_ADD_FILE = 0x0002 + FILE_APPEND_DATA = 0x0004 + FILE_ADD_SUBDIRECTORY = 0x0004 + FILE_CREATE_PIPE_INSTANCE = 0x0004 + FILE_READ_EA = 0x0008 + FILE_WRITE_EA = 0x0010 + FILE_EXECUTE = 0x0020 + FILE_TRAVERSE = 0x0020 + FILE_DELETE_CHILD = 0x0040 + FILE_READ_ATTRIBUTES = 0x0080 + FILE_WRITE_ATTRIBUTES = 0x0100 + DELETE = 0x00010000 + READ_CONTROL = 0x00020000 + WRITE_DAC = 0x00040000 + WRITE_OWNER = 0x00080000 + SYNCHRONIZE = 0x00100000 + STANDARD_RIGHTS_ALL = 0x001F0000 + + filemask = ldm & STANDARD_RIGHTS_ALL + + if (ldm & RIGHT_DS_READ_PROPERTY) and (ldm & RIGHT_DS_LIST_CONTENTS): + filemask = filemask | (SYNCHRONIZE | FILE_LIST_DIRECTORY | + FILE_READ_ATTRIBUTES | FILE_READ_EA | + FILE_READ_DATA | FILE_EXECUTE) + + if ldm & RIGHT_DS_WRITE_PROPERTY: + filemask = filemask | (SYNCHRONIZE | FILE_WRITE_DATA | + FILE_APPEND_DATA | FILE_WRITE_EA | + FILE_WRITE_ATTRIBUTES | FILE_ADD_FILE | + FILE_ADD_SUBDIRECTORY) + + if ldm & RIGHT_DS_CREATE_CHILD: + filemask = filemask | (FILE_ADD_SUBDIRECTORY | FILE_ADD_FILE) + + if ldm & RIGHT_DS_DELETE_CHILD: + filemask = filemask | FILE_DELETE_CHILD + + return filemask + + +def dsacl2fsacl(dssddl, sid, as_sddl=True): + """ + + This function takes an the SDDL representation of a DS + ACL and return the SDDL representation of this ACL adapted + for files. It's used for Policy object provision + """ + ref = security.descriptor.from_sddl(dssddl, sid) + fdescr = security.descriptor() + fdescr.owner_sid = ref.owner_sid + fdescr.group_sid = ref.group_sid + fdescr.type = ref.type + fdescr.revision = ref.revision + aces = ref.dacl.aces + for i in range(0, len(aces)): + ace = aces[i] + if not ace.type & security.SEC_ACE_TYPE_ACCESS_ALLOWED_OBJECT and str(ace.trustee) != security.SID_BUILTIN_PREW2K: + # if fdescr.type & security.SEC_DESC_DACL_AUTO_INHERITED: + ace.flags = ace.flags | security.SEC_ACE_FLAG_OBJECT_INHERIT | security.SEC_ACE_FLAG_CONTAINER_INHERIT + if str(ace.trustee) == security.SID_CREATOR_OWNER: + # For Creator/Owner the IO flag is set as this ACE has only a sense for child objects + ace.flags = ace.flags | security.SEC_ACE_FLAG_INHERIT_ONLY + ace.access_mask = ldapmask2filemask(ace.access_mask) + fdescr.dacl_add(ace) + + if not as_sddl: + return fdescr + + return fdescr.as_sddl(sid) diff --git a/python/samba/provision/__init__.py b/python/samba/provision/__init__.py new file mode 100644 index 00000000000..aac0ee36b2a --- /dev/null +++ b/python/samba/provision/__init__.py @@ -0,0 +1,2279 @@ +# Unix SMB/CIFS implementation. +# backend code for provisioning a Samba4 server + +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2012 +# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2008-2009 +# Copyright (C) Oliver Liebel <oliver@itc.li> 2008-2009 +# +# Based on the original in EJS: +# Copyright (C) Andrew Tridgell <tridge@samba.org> 2005 +# +# 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/>. +# + +"""Functions for setting up a Samba configuration.""" + +__docformat__ = "restructuredText" + +from base64 import b64encode +import os +import re +import pwd +import grp +import logging +import time +import uuid +import socket +import urllib +import string +import tempfile + +import ldb + +from samba.auth import system_session, admin_session +import samba +from samba.samba3 import smbd, passdb +from samba.samba3 import param as s3param +from samba.dsdb import DS_DOMAIN_FUNCTION_2000 +from samba import ( + Ldb, + MAX_NETBIOS_NAME_LEN, + check_all_substituted, + is_valid_netbios_char, + setup_file, + substitute_var, + valid_netbios_name, + version, + ) +from samba.dcerpc import security, misc +from samba.dcerpc.misc import ( + SEC_CHAN_BDC, + SEC_CHAN_WKSTA, + ) +from samba.dsdb import ( + DS_DOMAIN_FUNCTION_2003, + DS_DOMAIN_FUNCTION_2008_R2, + ENC_ALL_TYPES, + ) +from samba.idmap import IDmapDB +from samba.ms_display_specifiers import read_ms_ldif +from samba.ntacls import setntacl, getntacl, dsacl2fsacl +from samba.ndr import ndr_pack, ndr_unpack +from samba.provision.backend import ( + ExistingBackend, + FDSBackend, + LDBBackend, + OpenLDAPBackend, + ) +from samba.provision.descriptor import ( + get_empty_descriptor, + get_config_descriptor, + get_config_partitions_descriptor, + get_config_sites_descriptor, + get_config_ntds_quotas_descriptor, + get_config_delete_protected1_descriptor, + get_config_delete_protected1wd_descriptor, + get_config_delete_protected2_descriptor, + get_domain_descriptor, + get_domain_infrastructure_descriptor, + get_domain_builtin_descriptor, + get_domain_computers_descriptor, + get_domain_users_descriptor, + get_domain_controllers_descriptor, + get_domain_delete_protected1_descriptor, + get_domain_delete_protected2_descriptor, + get_dns_partition_descriptor, + get_dns_forest_microsoft_dns_descriptor, + get_dns_domain_microsoft_dns_descriptor, + ) +from samba.provision.common import ( + setup_path, + setup_add_ldif, + setup_modify_ldif, + ) +from samba.provision.sambadns import ( + get_dnsadmins_sid, + setup_ad_dns, + create_dns_update_list + ) + +import samba.param +import samba.registry +from samba.schema import Schema +from samba.samdb import SamDB +from samba.dbchecker import dbcheck + + +DEFAULT_POLICY_GUID = "31B2F340-016D-11D2-945F-00C04FB984F9" +DEFAULT_DC_POLICY_GUID = "6AC1786C-016F-11D2-945F-00C04fB984F9" +DEFAULTSITE = "Default-First-Site-Name" +LAST_PROVISION_USN_ATTRIBUTE = "lastProvisionUSN" + + +class ProvisionPaths(object): + + def __init__(self): + self.shareconf = None + self.hklm = None + self.hkcu = None + self.hkcr = None + self.hku = None + self.hkpd = None + self.hkpt = None + self.samdb = None + self.idmapdb = None + self.secrets = None + self.keytab = None + self.dns_keytab = None + self.dns = None + self.winsdb = None + self.private_dir = None + self.state_dir = None + + +class ProvisionNames(object): + + def __init__(self): + self.ncs = None + self.rootdn = None + self.domaindn = None + self.configdn = None + self.schemadn = None + self.dnsforestdn = None + self.dnsdomaindn = None + self.ldapmanagerdn = None + self.dnsdomain = None + self.realm = None + self.netbiosname = None + self.domain = None + self.hostname = None + self.sitename = None + self.smbconf = None + self.name_map = {} + + +def find_provision_key_parameters(samdb, secretsdb, idmapdb, paths, smbconf, + lp): + """Get key provision parameters (realm, domain, ...) from a given provision + + :param samdb: An LDB object connected to the sam.ldb file + :param secretsdb: An LDB object connected to the secrets.ldb file + :param idmapdb: An LDB object connected to the idmap.ldb file + :param paths: A list of path to provision object + :param smbconf: Path to the smb.conf file + :param lp: A LoadParm object + :return: A list of key provision parameters + """ + names = ProvisionNames() + names.adminpass = None + + # NT domain, kerberos realm, root dn, domain dn, domain dns name + names.domain = string.upper(lp.get("workgroup")) + names.realm = lp.get("realm") + names.dnsdomain = names.realm.lower() + basedn = samba.dn_from_dns_name(names.dnsdomain) + names.realm = string.upper(names.realm) + # netbiosname + # Get the netbiosname first (could be obtained from smb.conf in theory) + res = secretsdb.search(expression="(flatname=%s)" % + names.domain,base="CN=Primary Domains", + scope=ldb.SCOPE_SUBTREE, attrs=["sAMAccountName"]) + names.netbiosname = str(res[0]["sAMAccountName"]).replace("$","") + + names.smbconf = smbconf + + # That's a bit simplistic but it's ok as long as we have only 3 + # partitions + current = samdb.search(expression="(objectClass=*)", + base="", scope=ldb.SCOPE_BASE, + attrs=["defaultNamingContext", "schemaNamingContext", + "configurationNamingContext","rootDomainNamingContext", + "namingContexts"]) + + names.configdn = current[0]["configurationNamingContext"] + configdn = str(names.configdn) + names.schemadn = current[0]["schemaNamingContext"] + if not (ldb.Dn(samdb, basedn) == (ldb.Dn(samdb, + current[0]["defaultNamingContext"][0]))): + raise ProvisioningError(("basedn in %s (%s) and from %s (%s)" + "is not the same ..." % (paths.samdb, + str(current[0]["defaultNamingContext"][0]), + paths.smbconf, basedn))) + + names.domaindn=current[0]["defaultNamingContext"] + names.rootdn=current[0]["rootDomainNamingContext"] + names.ncs=current[0]["namingContexts"] + names.dnsforestdn = None + names.dnsdomaindn = None + + for i in range(0, len(names.ncs)): + nc = names.ncs[i] + + dnsforestdn = "DC=ForestDnsZones,%s" % (str(names.rootdn)) + if nc == dnsforestdn: + names.dnsforestdn = dnsforestdn + continue + + dnsdomaindn = "DC=DomainDnsZones,%s" % (str(names.domaindn)) + if nc == dnsdomaindn: + names.dnsdomaindn = dnsdomaindn + continue + + # default site name + res3 = samdb.search(expression="(objectClass=site)", + base="CN=Sites," + configdn, scope=ldb.SCOPE_ONELEVEL, attrs=["cn"]) + names.sitename = str(res3[0]["cn"]) + + # dns hostname and server dn + res4 = samdb.search(expression="(CN=%s)" % names.netbiosname, + base="OU=Domain Controllers,%s" % basedn, + scope=ldb.SCOPE_ONELEVEL, attrs=["dNSHostName"]) + names.hostname = str(res4[0]["dNSHostName"]).replace("." + names.dnsdomain, "") + + server_res = samdb.search(expression="serverReference=%s" % res4[0].dn, + attrs=[], base=configdn) + names.serverdn = server_res[0].dn + + # invocation id/objectguid + res5 = samdb.search(expression="(objectClass=*)", + base="CN=NTDS Settings,%s" % str(names.serverdn), + scope=ldb.SCOPE_BASE, + attrs=["invocationID", "objectGUID"]) + names.invocation = str(ndr_unpack(misc.GUID, res5[0]["invocationId"][0])) + names.ntdsguid = str(ndr_unpack(misc.GUID, res5[0]["objectGUID"][0])) + + # domain guid/sid + res6 = samdb.search(expression="(objectClass=*)", base=basedn, + scope=ldb.SCOPE_BASE, attrs=["objectGUID", + "objectSid","msDS-Behavior-Version" ]) + names.domainguid = str(ndr_unpack(misc.GUID, res6[0]["objectGUID"][0])) + names.domainsid = ndr_unpack( security.dom_sid, res6[0]["objectSid"][0]) + if res6[0].get("msDS-Behavior-Version") is None or \ + int(res6[0]["msDS-Behavior-Version"][0]) < DS_DOMAIN_FUNCTION_2000: + names.domainlevel = DS_DOMAIN_FUNCTION_2000 + else: + names.domainlevel = int(res6[0]["msDS-Behavior-Version"][0]) + + # policy guid + res7 = samdb.search(expression="(displayName=Default Domain Policy)", + base="CN=Policies,CN=System," + basedn, + scope=ldb.SCOPE_ONELEVEL, attrs=["cn","displayName"]) + names.policyid = str(res7[0]["cn"]).replace("{","").replace("}","") + # dc policy guid + res8 = samdb.search(expression="(displayName=Default Domain Controllers" + " Policy)", + base="CN=Policies,CN=System," + basedn, + scope=ldb.SCOPE_ONELEVEL, + attrs=["cn","displayName"]) + if len(res8) == 1: + names.policyid_dc = str(res8[0]["cn"]).replace("{","").replace("}","") + else: + names.policyid_dc = None + + res9 = idmapdb.search(expression="(cn=%s-%s)" % + (str(names.domainsid), security.DOMAIN_RID_ADMINISTRATOR), + attrs=["xidNumber", "type"]) + if len(res9) != 1: + raise ProvisioningError("Unable to find uid/gid for Domain Admins rid (%s-%s" % (str(names.domainsid), security.DOMAIN_RID_ADMINISTRATOR)) + if res9[0]["type"][0] == "ID_TYPE_BOTH": + names.root_gid = res9[0]["xidNumber"][0] + else: + names.root_gid = pwd.getpwuid(int(res9[0]["xidNumber"][0])).pw_gid + + res10 = samdb.search(expression="(samaccountname=dns)", + scope=ldb.SCOPE_SUBTREE, attrs=["dn"], + controls=["search_options:1:2"]) + if (len(res10) > 0): + has_legacy_dns_account = True + else: + has_legacy_dns_account = False + + res11 = samdb.search(expression="(samaccountname=dns-%s)" % names.netbiosname, + scope=ldb.SCOPE_SUBTREE, attrs=["dn"], + controls=["search_options:1:2"]) + if (len(res11) > 0): + has_dns_account = True + else: + has_dns_account = False + + if names.dnsdomaindn is not None: + if has_dns_account: + names.dns_backend = 'BIND9_DLZ' + else: + names.dns_backend = 'SAMBA_INTERNAL' + elif has_dns_account or has_legacy_dns_account: + names.dns_backend = 'BIND9_FLATFILE' + else: + names.dns_backend = 'NONE' + + dns_admins_sid = get_dnsadmins_sid(samdb, names.domaindn) + names.name_map['DnsAdmins'] = str(dns_admins_sid) + + return names + + +def update_provision_usn(samdb, low, high, id, replace=False): + """Update the field provisionUSN in sam.ldb + + This field is used to track range of USN modified by provision and + upgradeprovision. + This value is used afterward by next provision to figure out if + the field have been modified since last provision. + + :param samdb: An LDB object connect to sam.ldb + :param low: The lowest USN modified by this upgrade + :param high: The highest USN modified by this upgrade + :param id: The invocation id of the samba's dc + :param replace: A boolean indicating if the range should replace any + existing one or appended (default) + """ + + tab = [] + if not replace: + entry = samdb.search(base="@PROVISION", + scope=ldb.SCOPE_BASE, + attrs=[LAST_PROVISION_USN_ATTRIBUTE, "dn"]) + for e in entry[0][LAST_PROVISION_USN_ATTRIBUTE]: + if not re.search(';', e): + e = "%s;%s" % (e, id) + tab.append(str(e)) + + tab.append("%s-%s;%s" % (low, high, id)) + delta = ldb.Message() + delta.dn = ldb.Dn(samdb, "@PROVISION") + delta[LAST_PROVISION_USN_ATTRIBUTE] = ldb.MessageElement(tab, + ldb.FLAG_MOD_REPLACE, LAST_PROVISION_USN_ATTRIBUTE) + entry = samdb.search(expression='provisionnerID=*', + base="@PROVISION", scope=ldb.SCOPE_BASE, + attrs=["provisionnerID"]) + if len(entry) == 0 or len(entry[0]) == 0: + delta["provisionnerID"] = ldb.MessageElement(id, ldb.FLAG_MOD_ADD, "provisionnerID") + samdb.modify(delta) + + +def set_provision_usn(samdb, low, high, id): + """Set the field provisionUSN in sam.ldb + This field is used to track range of USN modified by provision and + upgradeprovision. + This value is used afterward by next provision to figure out if + the field have been modified since last provision. + + :param samdb: An LDB object connect to sam.ldb + :param low: The lowest USN modified by this upgrade + :param high: The highest USN modified by this upgrade + :param id: The invocationId of the provision""" + + tab = [] + tab.append("%s-%s;%s" % (low, high, id)) + + delta = ldb.Message() + delta.dn = ldb.Dn(samdb, "@PROVISION") + delta[LAST_PROVISION_USN_ATTRIBUTE] = ldb.MessageElement(tab, + ldb.FLAG_MOD_ADD, LAST_PROVISION_USN_ATTRIBUTE) + samdb.add(delta) + + +def get_max_usn(samdb,basedn): + """ This function return the biggest USN present in the provision + + :param samdb: A LDB object pointing to the sam.ldb + :param basedn: A string containing the base DN of the provision + (ie. DC=foo, DC=bar) + :return: The biggest USN in the provision""" + + res = samdb.search(expression="objectClass=*",base=basedn, + scope=ldb.SCOPE_SUBTREE,attrs=["uSNChanged"], + controls=["search_options:1:2", + "server_sort:1:1:uSNChanged", + "paged_results:1:1"]) + return res[0]["uSNChanged"] + + +def get_last_provision_usn(sam): + """Get USNs ranges modified by a provision or an upgradeprovision + + :param sam: An LDB object pointing to the sam.ldb + :return: a dictionary which keys are invocation id and values are an array + of integer representing the different ranges + """ + try: + entry = sam.search(expression="%s=*" % LAST_PROVISION_USN_ATTRIBUTE, + base="@PROVISION", scope=ldb.SCOPE_BASE, + attrs=[LAST_PROVISION_USN_ATTRIBUTE, "provisionnerID"]) + except ldb.LdbError, (ecode, emsg): + if ecode == ldb.ERR_NO_SUCH_OBJECT: + return None + raise + if len(entry) > 0: + myids = [] + range = {} + p = re.compile(r'-') + if entry[0].get("provisionnerID"): + for e in entry[0]["provisionnerID"]: + myids.append(str(e)) + for r in entry[0][LAST_PROVISION_USN_ATTRIBUTE]: + tab1 = str(r).split(';') + if len(tab1) == 2: + id = tab1[1] + else: + id = "default" + if (len(myids) > 0 and id not in myids): + continue + tab2 = p.split(tab1[0]) + if range.get(id) is None: + range[id] = [] + range[id].append(tab2[0]) + range[id].append(tab2[1]) + return range + else: + return None + + +class ProvisionResult(object): + """Result of a provision. + + :ivar server_role: The server role + :ivar paths: ProvisionPaths instance + :ivar domaindn: The domain dn, as string + """ + + def __init__(self): + self.server_role = None + self.paths = None + self.domaindn = None + self.lp = None + self.samdb = None + self.idmap = None + self.names = None + self.domainsid = None + self.adminpass_generated = None + self.adminpass = None + self.backend_result = None + + def report_logger(self, logger): + """Report this provision result to a logger.""" + logger.info( + "Once the above files are installed, your Samba4 server will " + "be ready to use") + if self.adminpass_generated: + logger.info("Admin password: %s", self.adminpass) + logger.info("Server Role: %s", self.server_role) + logger.info("Hostname: %s", self.names.hostname) + logger.info("NetBIOS Domain: %s", self.names.domain) + logger.info("DNS Domain: %s", self.names.dnsdomain) + logger.info("DOMAIN SID: %s", self.domainsid) + + if self.backend_result: + self.backend_result.report_logger(logger) + + +def check_install(lp, session_info, credentials): + """Check whether the current install seems ok. + + :param lp: Loadparm context + :param session_info: Session information + :param credentials: Credentials + """ + if lp.get("realm") == "": + raise Exception("Realm empty") + samdb = Ldb(lp.samdb_url(), session_info=session_info, + credentials=credentials, lp=lp) + if len(samdb.search("(cn=Administrator)")) != 1: + raise ProvisioningError("No administrator account found") + + +def findnss(nssfn, names): + """Find a user or group from a list of possibilities. + + :param nssfn: NSS Function to try (should raise KeyError if not found) + :param names: Names to check. + :return: Value return by first names list. + """ + for name in names: + try: + return nssfn(name) + except KeyError: + pass + raise KeyError("Unable to find user/group in %r" % names) + + +findnss_uid = lambda names: findnss(pwd.getpwnam, names)[2] +findnss_gid = lambda names: findnss(grp.getgrnam, names)[2] + + +def provision_paths_from_lp(lp, dnsdomain): + """Set the default paths for provisioning. + + :param lp: Loadparm context. + :param dnsdomain: DNS Domain name + """ + paths = ProvisionPaths() + paths.private_dir = lp.get("private dir") + paths.state_dir = lp.get("state directory") + + # This is stored without path prefix for the "privateKeytab" attribute in + # "secrets_dns.ldif". + paths.dns_keytab = "dns.keytab" + paths.keytab = "secrets.keytab" + + paths.shareconf = os.path.join(paths.private_dir, "share.ldb") + paths.samdb = os.path.join(paths.private_dir, "sam.ldb") + paths.idmapdb = os.path.join(paths.private_dir, "idmap.ldb") + paths.secrets = os.path.join(paths.private_dir, "secrets.ldb") + paths.privilege = os.path.join(paths.private_dir, "privilege.ldb") + paths.dns = os.path.join(paths.private_dir, "dns", dnsdomain + ".zone") + paths.dns_update_list = os.path.join(paths.private_dir, "dns_update_list") + paths.spn_update_list = os.path.join(paths.private_dir, "spn_update_list") + paths.namedconf = os.path.join(paths.private_dir, "named.conf") + paths.namedconf_update = os.path.join(paths.private_dir, "named.conf.update") + paths.namedtxt = os.path.join(paths.private_dir, "named.txt") + paths.krb5conf = os.path.join(paths.private_dir, "krb5.conf") + paths.winsdb = os.path.join(paths.private_dir, "wins.ldb") + paths.s4_ldapi_path = os.path.join(paths.private_dir, "ldapi") + paths.hklm = "hklm.ldb" + paths.hkcr = "hkcr.ldb" + paths.hkcu = "hkcu.ldb" + paths.hku = "hku.ldb" + paths.hkpd = "hkpd.ldb" + paths.hkpt = "hkpt.ldb" + paths.sysvol = lp.get("path", "sysvol") + paths.netlogon = lp.get("path", "netlogon") + paths.smbconf = lp.configfile + return paths + + +def determine_netbios_name(hostname): + """Determine a netbios name from a hostname.""" + # remove forbidden chars and force the length to be <16 + netbiosname = "".join([x for x in hostname if is_valid_netbios_char(x)]) + return netbiosname[:MAX_NETBIOS_NAME_LEN].upper() + + +def guess_names(lp=None, hostname=None, domain=None, dnsdomain=None, + serverrole=None, rootdn=None, domaindn=None, configdn=None, + schemadn=None, serverdn=None, sitename=None): + """Guess configuration settings to use.""" + + if hostname is None: + hostname = socket.gethostname().split(".")[0] + + netbiosname = lp.get("netbios name") + if netbiosname is None: + netbiosname = determine_netbios_name(hostname) + netbiosname = netbiosname.upper() + if not valid_netbios_name(netbiosname): + raise InvalidNetbiosName(netbiosname) + + if dnsdomain is None: + dnsdomain = lp.get("realm") + if dnsdomain is None or dnsdomain == "": + raise ProvisioningError("guess_names: 'realm' not specified in supplied %s!", lp.configfile) + + dnsdomain = dnsdomain.lower() + + if serverrole is None: + serverrole = lp.get("server role") + if serverrole is None: + raise ProvisioningError("guess_names: 'server role' not specified in supplied %s!" % lp.configfile) + + serverrole = serverrole.lower() + + realm = dnsdomain.upper() + + if lp.get("realm") == "": + raise ProvisioningError("guess_names: 'realm =' was not specified in supplied %s. Please remove the smb.conf file and let provision generate it" % lp.configfile) + + if lp.get("realm").upper() != realm: + raise ProvisioningError("guess_names: 'realm=%s' in %s must match chosen realm '%s'! Please remove the smb.conf file and let provision generate it" % (lp.get("realm").upper(), realm, lp.configfile)) + + if lp.get("server role").lower() != serverrole: + raise ProvisioningError("guess_names: 'server role=%s' in %s must match chosen server role '%s'! Please remove the smb.conf file and let provision generate it" % (lp.get("server role"), lp.configfile, serverrole)) + + if serverrole == "active directory domain controller": + if domain is None: + # This will, for better or worse, default to 'WORKGROUP' + domain = lp.get("workgroup") + domain = domain.upper() + + if lp.get("workgroup").upper() != domain: + raise ProvisioningError("guess_names: Workgroup '%s' in smb.conf must match chosen domain '%s'! Please remove the %s file and let provision generate it" % (lp.get("workgroup").upper(), domain, lp.configfile)) + + if domaindn is None: + domaindn = samba.dn_from_dns_name(dnsdomain) + + if domain == netbiosname: + raise ProvisioningError("guess_names: Domain '%s' must not be equal to short host name '%s'!" % (domain, netbiosname)) + else: + domain = netbiosname + if domaindn is None: + domaindn = "DC=" + netbiosname + + if not valid_netbios_name(domain): + raise InvalidNetbiosName(domain) + + if hostname.upper() == realm: + raise ProvisioningError("guess_names: Realm '%s' must not be equal to hostname '%s'!" % (realm, hostname)) + if netbiosname.upper() == realm: + raise ProvisioningError("guess_names: Realm '%s' must not be equal to netbios hostname '%s'!" % (realm, netbiosname)) + if domain == realm: + raise ProvisioningError("guess_names: Realm '%s' must not be equal to short domain name '%s'!" % (realm, domain)) + + if rootdn is None: + rootdn = domaindn + + if configdn is None: + configdn = "CN=Configuration," + rootdn + if schemadn is None: + schemadn = "CN=Schema," + configdn + + if sitename is None: + sitename = DEFAULTSITE + + names = ProvisionNames() + names.rootdn = rootdn + names.domaindn = domaindn + names.configdn = configdn + names.schemadn = schemadn + names.ldapmanagerdn = "CN=Manager," + rootdn + names.dnsdomain = dnsdomain + names.domain = domain + names.realm = realm + names.netbiosname = netbiosname + names.hostname = hostname + names.sitename = sitename + names.serverdn = "CN=%s,CN=Servers,CN=%s,CN=Sites,%s" % ( + netbiosname, sitename, configdn) + + return names + + +def make_smbconf(smbconf, hostname, domain, realm, targetdir, + serverrole=None, eadb=False, use_ntvfs=False, lp=None, + global_param=None): + """Create a new smb.conf file based on a couple of basic settings. + """ + assert smbconf is not None + + if hostname is None: + hostname = socket.gethostname().split(".")[0] + + netbiosname = determine_netbios_name(hostname) + + if serverrole is None: + serverrole = "standalone server" + + assert domain is not None + domain = domain.upper() + + assert realm is not None + realm = realm.upper() + + global_settings = { + "netbios name": netbiosname, + "workgroup": domain, + "realm": realm, + "server role": serverrole, + } + + if lp is None: + lp = samba.param.LoadParm() + #Load non-existent file + if os.path.exists(smbconf): + lp.load(smbconf) + + if global_param is not None: + for ent in global_param: + if global_param[ent] is not None: + global_settings[ent] = " ".join(global_param[ent]) + + if targetdir is not None: + global_settings["private dir"] = os.path.abspath(os.path.join(targetdir, "private")) + global_settings["lock dir"] = os.path.abspath(targetdir) + global_settings["state directory"] = os.path.abspath(os.path.join(targetdir, "state")) + global_settings["cache directory"] = os.path.abspath(os.path.join(targetdir, "cache")) + + lp.set("lock dir", os.path.abspath(targetdir)) + lp.set("state directory", global_settings["state directory"]) + lp.set("cache directory", global_settings["cache directory"]) + + if eadb: + if use_ntvfs and not lp.get("posix:eadb"): + if targetdir is not None: + privdir = os.path.join(targetdir, "private") + else: + privdir = lp.get("private dir") + lp.set("posix:eadb", os.path.abspath(os.path.join(privdir, "eadb.tdb"))) + elif not use_ntvfs and not lp.get("xattr_tdb:file"): + if targetdir is not None: + statedir = os.path.join(targetdir, "state") + else: + statedir = lp.get("state directory") + lp.set("xattr_tdb:file", os.path.abspath(os.path.join(statedir, "xattr.tdb"))) + + shares = {} + if serverrole == "active directory domain controller": + shares["sysvol"] = os.path.join(lp.get("state directory"), "sysvol") + shares["netlogon"] = os.path.join(shares["sysvol"], realm.lower(), + "scripts") + else: + global_settings["passdb backend"] = "samba_dsdb" + + f = open(smbconf, 'w') + try: + f.write("[globals]\n") + for key, val in global_settings.iteritems(): + f.write("\t%s = %s\n" % (key, val)) + f.write("\n") + + for name, path in shares.iteritems(): + f.write("[%s]\n" % name) + f.write("\tpath = %s\n" % path) + f.write("\tread only = no\n") + f.write("\n") + finally: + f.close() + # reload the smb.conf + lp.load(smbconf) + + # and dump it without any values that are the default + # this ensures that any smb.conf parameters that were set + # on the provision/join command line are set in the resulting smb.conf + f = open(smbconf, mode='w') + try: + lp.dump(f, False) + finally: + f.close() + + +def setup_name_mappings(idmap, sid, root_uid, nobody_uid, + users_gid, root_gid): + """setup reasonable name mappings for sam names to unix names. + + :param samdb: SamDB object. + :param idmap: IDmap db object. + :param sid: The domain sid. + :param domaindn: The domain DN. + :param root_uid: uid of the UNIX root user. + :param nobody_uid: uid of the UNIX nobody user. + :param users_gid: gid of the UNIX users group. + :param root_gid: gid of the UNIX root group. + """ + idmap.setup_name_mapping("S-1-5-7", idmap.TYPE_UID, nobody_uid) + + idmap.setup_name_mapping(sid + "-500", idmap.TYPE_UID, root_uid) + idmap.setup_name_mapping(sid + "-513", idmap.TYPE_GID, users_gid) + + +def setup_samdb_partitions(samdb_path, logger, lp, session_info, + provision_backend, names, schema, serverrole, + erase=False): + """Setup the partitions for the SAM database. + + Alternatively, provision() may call this, and then populate the database. + + :note: This will wipe the Sam Database! + + :note: This function always removes the local SAM LDB file. The erase + parameter controls whether to erase the existing data, which + may not be stored locally but in LDAP. + + """ + assert session_info is not None + + # We use options=["modules:"] to stop the modules loading - we + # just want to wipe and re-initialise the database, not start it up + + try: + os.unlink(samdb_path) + except OSError: + pass + + samdb = Ldb(url=samdb_path, session_info=session_info, + lp=lp, options=["modules:"]) + + ldap_backend_line = "# No LDAP backend" + if provision_backend.type != "ldb": + ldap_backend_line = "ldapBackend: %s" % provision_backend.ldap_uri + + samdb.transaction_start() + try: + logger.info("Setting up sam.ldb partitions and settings") + setup_add_ldif(samdb, setup_path("provision_partitions.ldif"), { + "LDAP_BACKEND_LINE": ldap_backend_line + }) + + + setup_add_ldif(samdb, setup_path("provision_init.ldif"), { + "BACKEND_TYPE": provision_backend.type, + "SERVER_ROLE": serverrole + }) + + logger.info("Setting up sam.ldb rootDSE") + setup_samdb_rootdse(samdb, names) + except: + samdb.transaction_cancel() + raise + else: + samdb.transaction_commit() + + +def secretsdb_self_join(secretsdb, domain, + netbiosname, machinepass, domainsid=None, + realm=None, dnsdomain=None, + keytab_path=None, + key_version_number=1, + secure_channel_type=SEC_CHAN_WKSTA): + """Add domain join-specific bits to a secrets database. + + :param secretsdb: Ldb Handle to the secrets database + :param machinepass: Machine password + """ + attrs = ["whenChanged", + "secret", + "priorSecret", + "priorChanged", + "krb5Keytab", + "privateKeytab"] + + if realm is not None: + if dnsdomain is None: + dnsdomain = realm.lower() + dnsname = '%s.%s' % (netbiosname.lower(), dnsdomain.lower()) + else: + dnsname = None + shortname = netbiosname.lower() + + # We don't need to set msg["flatname"] here, because rdn_name will handle + # it, and it causes problems for modifies anyway + msg = ldb.Message(ldb.Dn(secretsdb, "flatname=%s,cn=Primary Domains" % domain)) + msg["secureChannelType"] = [str(secure_channel_type)] + msg["objectClass"] = ["top", "primaryDomain"] + if dnsname is not None: + msg["objectClass"] = ["top", "primaryDomain", "kerberosSecret"] + msg["realm"] = [realm] + msg["saltPrincipal"] = ["host/%s@%s" % (dnsname, realm.upper())] + msg["msDS-KeyVersionNumber"] = [str(key_version_number)] + msg["privateKeytab"] = ["secrets.keytab"] + + msg["secret"] = [machinepass] + msg["samAccountName"] = ["%s$" % netbiosname] + msg["secureChannelType"] = [str(secure_channel_type)] + if domainsid is not None: + msg["objectSid"] = [ndr_pack(domainsid)] + + # This complex expression tries to ensure that we don't have more + # than one record for this SID, realm or netbios domain at a time, + # but we don't delete the old record that we are about to modify, + # because that would delete the keytab and previous password. + res = secretsdb.search(base="cn=Primary Domains", attrs=attrs, + expression=("(&(|(flatname=%s)(realm=%s)(objectSid=%s))(objectclass=primaryDomain)(!(distinguishedName=%s)))" % (domain, realm, str(domainsid), str(msg.dn))), + scope=ldb.SCOPE_ONELEVEL) + + for del_msg in res: + secretsdb.delete(del_msg.dn) + + res = secretsdb.search(base=msg.dn, attrs=attrs, scope=ldb.SCOPE_BASE) + + if len(res) == 1: + msg["priorSecret"] = [res[0]["secret"][0]] + msg["priorWhenChanged"] = [res[0]["whenChanged"][0]] + + try: + msg["privateKeytab"] = [res[0]["privateKeytab"][0]] + except KeyError: + pass + + try: + msg["krb5Keytab"] = [res[0]["krb5Keytab"][0]] + except KeyError: + pass + + for el in msg: + if el != 'dn': + msg[el].set_flags(ldb.FLAG_MOD_REPLACE) + secretsdb.modify(msg) + secretsdb.rename(res[0].dn, msg.dn) + else: + spn = [ 'HOST/%s' % shortname ] + if secure_channel_type == SEC_CHAN_BDC and dnsname is not None: + # we are a domain controller then we add servicePrincipalName + # entries for the keytab code to update. + spn.extend([ 'HOST/%s' % dnsname ]) + msg["servicePrincipalName"] = spn + + secretsdb.add(msg) + + +def setup_secretsdb(paths, session_info, backend_credentials, lp): + """Setup the secrets database. + + :note: This function does not handle exceptions and transaction on purpose, + it's up to the caller to do this job. + + :param path: Path to the secrets database. + :param session_info: Session info. + :param credentials: Credentials + :param lp: Loadparm context + :return: LDB handle for the created secrets database + """ + if os.path.exists(paths.secrets): + os.unlink(paths.secrets) + + keytab_path = os.path.join(paths.private_dir, paths.keytab) + if os.path.exists(keytab_path): + os.unlink(keytab_path) + + dns_keytab_path = os.path.join(paths.private_dir, paths.dns_keytab) + if os.path.exists(dns_keytab_path): + os.unlink(dns_keytab_path) + + path = paths.secrets + + secrets_ldb = Ldb(path, session_info=session_info, lp=lp) + secrets_ldb.erase() + secrets_ldb.load_ldif_file_add(setup_path("secrets_init.ldif")) + secrets_ldb = Ldb(path, session_info=session_info, lp=lp) + secrets_ldb.transaction_start() + try: + secrets_ldb.load_ldif_file_add(setup_path("secrets.ldif")) + + if (backend_credentials is not None and + backend_credentials.authentication_requested()): + if backend_credentials.get_bind_dn() is not None: + setup_add_ldif(secrets_ldb, + setup_path("secrets_simple_ldap.ldif"), { + "LDAPMANAGERDN": backend_credentials.get_bind_dn(), + "LDAPMANAGERPASS_B64": b64encode(backend_credentials.get_password()) + }) + else: + setup_add_ldif(secrets_ldb, + setup_path("secrets_sasl_ldap.ldif"), { + "LDAPADMINUSER": backend_credentials.get_username(), + "LDAPADMINREALM": backend_credentials.get_realm(), + "LDAPADMINPASS_B64": b64encode(backend_credentials.get_password()) + }) + except: + secrets_ldb.transaction_cancel() + raise + return secrets_ldb + + +def setup_privileges(path, session_info, lp): + """Setup the privileges database. + + :param path: Path to the privileges database. + :param session_info: Session info. + :param credentials: Credentials + :param lp: Loadparm context + :return: LDB handle for the created secrets database + """ + if os.path.exists(path): + os.unlink(path) + privilege_ldb = Ldb(path, session_info=session_info, lp=lp) + privilege_ldb.erase() + privilege_ldb.load_ldif_file_add(setup_path("provision_privilege.ldif")) + + +def setup_registry(path, session_info, lp): + """Setup the registry. + + :param path: Path to the registry database + :param session_info: Session information + :param credentials: Credentials + :param lp: Loadparm context + """ + reg = samba.registry.Registry() + hive = samba.registry.open_ldb(path, session_info=session_info, lp_ctx=lp) + reg.mount_hive(hive, samba.registry.HKEY_LOCAL_MACHINE) + provision_reg = setup_path("provision.reg") + assert os.path.exists(provision_reg) + reg.diff_apply(provision_reg) + + +def setup_idmapdb(path, session_info, lp): + """Setup the idmap database. + + :param path: path to the idmap database + :param session_info: Session information + :param credentials: Credentials + :param lp: Loadparm context + """ + if os.path.exists(path): + os.unlink(path) + + idmap_ldb = IDmapDB(path, session_info=session_info, lp=lp) + idmap_ldb.erase() + idmap_ldb.load_ldif_file_add(setup_path("idmap_init.ldif")) + return idmap_ldb + + +def setup_samdb_rootdse(samdb, names): + """Setup the SamDB rootdse. + + :param samdb: Sam Database handle + """ + setup_add_ldif(samdb, setup_path("provision_rootdse_add.ldif"), { + "SCHEMADN": names.schemadn, + "DOMAINDN": names.domaindn, + "ROOTDN" : names.rootdn, + "CONFIGDN": names.configdn, + "SERVERDN": names.serverdn, + }) + + +def setup_self_join(samdb, admin_session_info, names, fill, machinepass, + dns_backend, dnspass, domainsid, next_rid, invocationid, + policyguid, policyguid_dc, + domainControllerFunctionality, ntdsguid=None, dc_rid=None): + """Join a host to its own domain.""" + assert isinstance(invocationid, str) + if ntdsguid is not None: + ntdsguid_line = "objectGUID: %s\n"%ntdsguid + else: + ntdsguid_line = "" + + if dc_rid is None: + dc_rid = next_rid + + setup_add_ldif(samdb, setup_path("provision_self_join.ldif"), { + "CONFIGDN": names.configdn, + "SCHEMADN": names.schemadn, + "DOMAINDN": names.domaindn, + "SERVERDN": names.serverdn, + "INVOCATIONID": invocationid, + "NETBIOSNAME": names.netbiosname, + "DNSNAME": "%s.%s" % (names.hostname, names.dnsdomain), + "MACHINEPASS_B64": b64encode(machinepass.encode('utf-16-le')), + "DOMAINSID": str(domainsid), + "DCRID": str(dc_rid), + "SAMBA_VERSION_STRING": version, + "NTDSGUID": ntdsguid_line, + "DOMAIN_CONTROLLER_FUNCTIONALITY": str( + domainControllerFunctionality), + "RIDALLOCATIONSTART": str(next_rid + 100), + "RIDALLOCATIONEND": str(next_rid + 100 + 499)}) + + setup_add_ldif(samdb, setup_path("provision_group_policy.ldif"), { + "POLICYGUID": policyguid, + "POLICYGUID_DC": policyguid_dc, + "DNSDOMAIN": names.dnsdomain, + "DOMAINDN": names.domaindn}) + + # If we are setting up a subdomain, then this has been replicated in, so we + # don't need to add it + if fill == FILL_FULL: + setup_add_ldif(samdb, setup_path("provision_self_join_config.ldif"), { + "CONFIGDN": names.configdn, + "SCHEMADN": names.schemadn, + "DOMAINDN": names.domaindn, + "SERVERDN": names.serverdn, + "INVOCATIONID": invocationid, + "NETBIOSNAME": names.netbiosname, + "DNSNAME": "%s.%s" % (names.hostname, names.dnsdomain), + "MACHINEPASS_B64": b64encode(machinepass.encode('utf-16-le')), + "DOMAINSID": str(domainsid), + "DCRID": str(dc_rid), + "SAMBA_VERSION_STRING": version, + "NTDSGUID": ntdsguid_line, + "DOMAIN_CONTROLLER_FUNCTIONALITY": str( + domainControllerFunctionality)}) + + # Setup fSMORoleOwner entries to point at the newly created DC entry + setup_modify_ldif(samdb, + setup_path("provision_self_join_modify_config.ldif"), { + "CONFIGDN": names.configdn, + "SCHEMADN": names.schemadn, + "DEFAULTSITE": names.sitename, + "NETBIOSNAME": names.netbiosname, + "SERVERDN": names.serverdn, + }) + + system_session_info = system_session() + samdb.set_session_info(system_session_info) + # Setup fSMORoleOwner entries to point at the newly created DC entry to + # modify a serverReference under cn=config when we are a subdomain, we must + # be system due to ACLs + setup_modify_ldif(samdb, setup_path("provision_self_join_modify.ldif"), { + "DOMAINDN": names.domaindn, + "SERVERDN": names.serverdn, + "NETBIOSNAME": names.netbiosname, + }) + + samdb.set_session_info(admin_session_info) + + if dns_backend != "SAMBA_INTERNAL": + # This is Samba4 specific and should be replaced by the correct + # DNS AD-style setup + setup_add_ldif(samdb, setup_path("provision_dns_add_samba.ldif"), { + "DNSDOMAIN": names.dnsdomain, + "DOMAINDN": names.domaindn, + "DNSPASS_B64": b64encode(dnspass.encode('utf-16-le')), + "HOSTNAME" : names.hostname, + "DNSNAME" : '%s.%s' % ( + names.netbiosname.lower(), names.dnsdomain.lower()) + }) + + +def getpolicypath(sysvolpath, dnsdomain, guid): + """Return the physical path of policy given its guid. + + :param sysvolpath: Path to the sysvol folder + :param dnsdomain: DNS name of the AD domain + :param guid: The GUID of the policy + :return: A string with the complete path to the policy folder + """ + if guid[0] != "{": + guid = "{%s}" % guid + policy_path = os.path.join(sysvolpath, dnsdomain, "Policies", guid) + return policy_path + + +def create_gpo_struct(policy_path): + if not os.path.exists(policy_path): + os.makedirs(policy_path, 0775) + f = open(os.path.join(policy_path, "GPT.INI"), 'w') + try: + f.write("[General]\r\nVersion=0") + finally: + f.close() + p = os.path.join(policy_path, "MACHINE") + if not os.path.exists(p): + os.makedirs(p, 0775) + p = os.path.join(policy_path, "USER") + if not os.path.exists(p): + os.makedirs(p, 0775) + + +def create_default_gpo(sysvolpath, dnsdomain, policyguid, policyguid_dc): + """Create the default GPO for a domain + + :param sysvolpath: Physical path for the sysvol folder + :param dnsdomain: DNS domain name of the AD domain + :param policyguid: GUID of the default domain policy + :param policyguid_dc: GUID of the default domain controler policy + """ + policy_path = getpolicypath(sysvolpath,dnsdomain,policyguid) + create_gpo_struct(policy_path) + + policy_path = getpolicypath(sysvolpath,dnsdomain,policyguid_dc) + create_gpo_struct(policy_path) + + +def setup_samdb(path, session_info, provision_backend, lp, names, + logger, fill, serverrole, schema, am_rodc=False): + """Setup a complete SAM Database. + + :note: This will wipe the main SAM database file! + """ + + # Also wipes the database + setup_samdb_partitions(path, logger=logger, lp=lp, + provision_backend=provision_backend, session_info=session_info, + names=names, serverrole=serverrole, schema=schema) + + # Load the database, but don's load the global schema and don't connect + # quite yet + samdb = SamDB(session_info=session_info, url=None, auto_connect=False, + credentials=provision_backend.credentials, lp=lp, + global_schema=False, am_rodc=am_rodc) + + logger.info("Pre-loading the Samba 4 and AD schema") + + # Load the schema from the one we computed earlier + samdb.set_schema(schema, write_indices_and_attributes=False) + + # Set the NTDS settings DN manually - in order to have it already around + # before the provisioned tree exists and we connect + samdb.set_ntds_settings_dn("CN=NTDS Settings,%s" % names.serverdn) + + # And now we can connect to the DB - the schema won't be loaded from the + # DB + samdb.connect(path) + + # But we have to give it one more kick to have it use the schema + # during provision - it needs, now that it is connected, to write + # the schema @ATTRIBUTES and @INDEXLIST records to the database. + samdb.set_schema(schema, write_indices_and_attributes=True) + + return samdb + + +def fill_samdb(samdb, lp, names, logger, domainsid, domainguid, policyguid, + policyguid_dc, fill, adminpass, krbtgtpass, machinepass, dns_backend, + dnspass, invocationid, ntdsguid, serverrole, am_rodc=False, + dom_for_fun_level=None, schema=None, next_rid=None, dc_rid=None): + + if next_rid is None: + next_rid = 1000 + + # Provision does not make much sense values larger than 1000000000 + # as the upper range of the rIDAvailablePool is 1073741823 and + # we don't want to create a domain that cannot allocate rids. + if next_rid < 1000 or next_rid > 1000000000: + error = "You want to run SAMBA 4 with a next_rid of %u, " % (next_rid) + error += "the valid range is %u-%u. The default is %u." % ( + 1000, 1000000000, 1000) + raise ProvisioningError(error) + + # ATTENTION: Do NOT change these default values without discussion with the + # team and/or release manager. They have a big impact on the whole program! + domainControllerFunctionality = DS_DOMAIN_FUNCTION_2008_R2 + + if dom_for_fun_level is None: + dom_for_fun_level = DS_DOMAIN_FUNCTION_2003 + + if dom_for_fun_level > domainControllerFunctionality: + raise ProvisioningError("You want to run SAMBA 4 on a domain and forest function level which itself is higher than its actual DC function level (2008_R2). This won't work!") + + domainFunctionality = dom_for_fun_level + forestFunctionality = dom_for_fun_level + + # Set the NTDS settings DN manually - in order to have it already around + # before the provisioned tree exists and we connect + samdb.set_ntds_settings_dn("CN=NTDS Settings,%s" % names.serverdn) + + samdb.transaction_start() + try: + # Set the domain functionality levels onto the database. + # Various module (the password_hash module in particular) need + # to know what level of AD we are emulating. + + # These will be fixed into the database via the database + # modifictions below, but we need them set from the start. + samdb.set_opaque_integer("domainFunctionality", domainFunctionality) + samdb.set_opaque_integer("forestFunctionality", forestFunctionality) + samdb.set_opaque_integer("domainControllerFunctionality", + domainControllerFunctionality) + + samdb.set_domain_sid(str(domainsid)) + samdb.set_invocation_id(invocationid) + + logger.info("Adding DomainDN: %s" % names.domaindn) + + # impersonate domain admin + admin_session_info = admin_session(lp, str(domainsid)) + samdb.set_session_info(admin_session_info) + if domainguid is not None: + domainguid_line = "objectGUID: %s\n-" % domainguid + else: + domainguid_line = "" + + descr = b64encode(get_domain_descriptor(domainsid)) + setup_add_ldif(samdb, setup_path("provision_basedn.ldif"), { + "DOMAINDN": names.domaindn, + "DOMAINSID": str(domainsid), + "DESCRIPTOR": descr, + "DOMAINGUID": domainguid_line + }) + + setup_modify_ldif(samdb, setup_path("provision_basedn_modify.ldif"), { + "DOMAINDN": names.domaindn, + "CREATTIME": str(samba.unix2nttime(int(time.time()))), + "NEXTRID": str(next_rid), + "DEFAULTSITE": names.sitename, + "CONFIGDN": names.configdn, + "POLICYGUID": policyguid, + "DOMAIN_FUNCTIONALITY": str(domainFunctionality), + "SAMBA_VERSION_STRING": version + }) + + # If we are setting up a subdomain, then this has been replicated in, so we don't need to add it + if fill == FILL_FULL: + logger.info("Adding configuration container") + descr = b64encode(get_config_descriptor(domainsid)) + setup_add_ldif(samdb, setup_path("provision_configuration_basedn.ldif"), { + "CONFIGDN": names.configdn, + "DESCRIPTOR": descr, + }) + + # The LDIF here was created when the Schema object was constructed + logger.info("Setting up sam.ldb schema") + samdb.add_ldif(schema.schema_dn_add, controls=["relax:0"]) + samdb.modify_ldif(schema.schema_dn_modify) + samdb.write_prefixes_from_schema() + samdb.add_ldif(schema.schema_data, controls=["relax:0"]) + setup_add_ldif(samdb, setup_path("aggregate_schema.ldif"), + {"SCHEMADN": names.schemadn}) + + # Now register this container in the root of the forest + msg = ldb.Message(ldb.Dn(samdb, names.domaindn)) + msg["subRefs"] = ldb.MessageElement(names.configdn , ldb.FLAG_MOD_ADD, + "subRefs") + + except: + samdb.transaction_cancel() + raise + else: + samdb.transaction_commit() + + samdb.transaction_start() + try: + samdb.invocation_id = invocationid + + # If we are setting up a subdomain, then this has been replicated in, so we don't need to add it + if fill == FILL_FULL: + logger.info("Setting up sam.ldb configuration data") + + partitions_descr = b64encode(get_config_partitions_descriptor(domainsid)) + sites_descr = b64encode(get_config_sites_descriptor(domainsid)) + ntdsquotas_descr = b64encode(get_config_ntds_quotas_descriptor(domainsid)) + protected1_descr = b64encode(get_config_delete_protected1_descriptor(domainsid)) + protected1wd_descr = b64encode(get_config_delete_protected1wd_descriptor(domainsid)) + protected2_descr = b64encode(get_config_delete_protected2_descriptor(domainsid)) + + setup_add_ldif(samdb, setup_path("provision_configuration.ldif"), { + "CONFIGDN": names.configdn, + "NETBIOSNAME": names.netbiosname, + "DEFAULTSITE": names.sitename, + "DNSDOMAIN": names.dnsdomain, + "DOMAIN": names.domain, + "SCHEMADN": names.schemadn, + "DOMAINDN": names.domaindn, + "SERVERDN": names.serverdn, + "FOREST_FUNCTIONALITY": str(forestFunctionality), + "DOMAIN_FUNCTIONALITY": str(domainFunctionality), + "NTDSQUOTAS_DESCRIPTOR": ntdsquotas_descr, + "LOSTANDFOUND_DESCRIPTOR": protected1wd_descr, + "SERVICES_DESCRIPTOR": protected1_descr, + "PHYSICALLOCATIONS_DESCRIPTOR": protected1wd_descr, + "FORESTUPDATES_DESCRIPTOR": protected1wd_descr, + "EXTENDEDRIGHTS_DESCRIPTOR": protected2_descr, + "PARTITIONS_DESCRIPTOR": partitions_descr, + "SITES_DESCRIPTOR": sites_descr, + }) + + logger.info("Setting up display specifiers") + display_specifiers_ldif = read_ms_ldif( + setup_path('display-specifiers/DisplaySpecifiers-Win2k8R2.txt')) + display_specifiers_ldif = substitute_var(display_specifiers_ldif, + {"CONFIGDN": names.configdn}) + check_all_substituted(display_specifiers_ldif) + samdb.add_ldif(display_specifiers_ldif) + + logger.info("Modifying display specifiers") + setup_modify_ldif(samdb, + setup_path("provision_configuration_modify.ldif"), { + "CONFIGDN": names.configdn, + "DISPLAYSPECIFIERS_DESCRIPTOR": protected2_descr + }) + + logger.info("Adding users container") + users_desc = b64encode(get_domain_users_descriptor(domainsid)) + setup_add_ldif(samdb, setup_path("provision_users_add.ldif"), { + "DOMAINDN": names.domaindn, + "USERS_DESCRIPTOR": users_desc + }) + logger.info("Modifying users container") + setup_modify_ldif(samdb, setup_path("provision_users_modify.ldif"), { + "DOMAINDN": names.domaindn}) + logger.info("Adding computers container") + computers_desc = b64encode(get_domain_computers_descriptor(domainsid)) + setup_add_ldif(samdb, setup_path("provision_computers_add.ldif"), { + "DOMAINDN": names.domaindn, + "COMPUTERS_DESCRIPTOR": computers_desc + }) + logger.info("Modifying computers container") + setup_modify_ldif(samdb, + setup_path("provision_computers_modify.ldif"), { + "DOMAINDN": names.domaindn}) + logger.info("Setting up sam.ldb data") + infrastructure_desc = b64encode(get_domain_infrastructure_descriptor(domainsid)) + lostandfound_desc = b64encode(get_domain_delete_protected2_descriptor(domainsid)) + system_desc = b64encode(get_domain_delete_protected1_descriptor(domainsid)) + builtin_desc = b64encode(get_domain_builtin_descriptor(domainsid)) + controllers_desc = b64encode(get_domain_controllers_descriptor(domainsid)) + setup_add_ldif(samdb, setup_path("provision.ldif"), { + "CREATTIME": str(samba.unix2nttime(int(time.time()))), + "DOMAINDN": names.domaindn, + "NETBIOSNAME": names.netbiosname, + "DEFAULTSITE": names.sitename, + "CONFIGDN": names.configdn, + "SERVERDN": names.serverdn, + "RIDAVAILABLESTART": str(next_rid + 600), + "POLICYGUID_DC": policyguid_dc, + "INFRASTRUCTURE_DESCRIPTOR": infrastructure_desc, + "LOSTANDFOUND_DESCRIPTOR": lostandfound_desc, + "SYSTEM_DESCRIPTOR": system_desc, + "BUILTIN_DESCRIPTOR": builtin_desc, + "DOMAIN_CONTROLLERS_DESCRIPTOR": controllers_desc, + }) + + # If we are setting up a subdomain, then this has been replicated in, so we don't need to add it + if fill == FILL_FULL: + setup_modify_ldif(samdb, + setup_path("provision_configuration_references.ldif"), { + "CONFIGDN": names.configdn, + "SCHEMADN": names.schemadn}) + + logger.info("Setting up well known security principals") + protected1wd_descr = b64encode(get_config_delete_protected1wd_descriptor(domainsid)) + setup_add_ldif(samdb, setup_path("provision_well_known_sec_princ.ldif"), { + "CONFIGDN": names.configdn, + "WELLKNOWNPRINCIPALS_DESCRIPTOR": protected1wd_descr, + }) + + if fill == FILL_FULL or fill == FILL_SUBDOMAIN: + setup_modify_ldif(samdb, + setup_path("provision_basedn_references.ldif"), + {"DOMAINDN": names.domaindn}) + + logger.info("Setting up sam.ldb users and groups") + setup_add_ldif(samdb, setup_path("provision_users.ldif"), { + "DOMAINDN": names.domaindn, + "DOMAINSID": str(domainsid), + "ADMINPASS_B64": b64encode(adminpass.encode('utf-16-le')), + "KRBTGTPASS_B64": b64encode(krbtgtpass.encode('utf-16-le')) + }) + + logger.info("Setting up self join") + setup_self_join(samdb, admin_session_info, names=names, fill=fill, + invocationid=invocationid, + dns_backend=dns_backend, + dnspass=dnspass, + machinepass=machinepass, + domainsid=domainsid, + next_rid=next_rid, + dc_rid=dc_rid, + policyguid=policyguid, + policyguid_dc=policyguid_dc, + domainControllerFunctionality=domainControllerFunctionality, + ntdsguid=ntdsguid) + + ntds_dn = "CN=NTDS Settings,%s" % names.serverdn + names.ntdsguid = samdb.searchone(basedn=ntds_dn, + attribute="objectGUID", expression="", scope=ldb.SCOPE_BASE) + assert isinstance(names.ntdsguid, str) + except: + samdb.transaction_cancel() + raise + else: + samdb.transaction_commit() + return samdb + + +FILL_FULL = "FULL" +FILL_SUBDOMAIN = "SUBDOMAIN" +FILL_NT4SYNC = "NT4SYNC" +FILL_DRS = "DRS" +SYSVOL_ACL = "O:LAG:BAD:P(A;OICI;0x001f01ff;;;BA)(A;OICI;0x001200a9;;;SO)(A;OICI;0x001f01ff;;;SY)(A;OICI;0x001200a9;;;AU)" +POLICIES_ACL = "O:LAG:BAD:P(A;OICI;0x001f01ff;;;BA)(A;OICI;0x001200a9;;;SO)(A;OICI;0x001f01ff;;;SY)(A;OICI;0x001200a9;;;AU)(A;OICI;0x001301bf;;;PA)" +SYSVOL_SERVICE="sysvol" + +def set_dir_acl(path, acl, lp, domsid, use_ntvfs, passdb, service=SYSVOL_SERVICE): + setntacl(lp, path, acl, domsid, use_ntvfs=use_ntvfs, skip_invalid_chown=True, passdb=passdb, service=service) + for root, dirs, files in os.walk(path, topdown=False): + for name in files: + setntacl(lp, os.path.join(root, name), acl, domsid, + use_ntvfs=use_ntvfs, skip_invalid_chown=True, passdb=passdb, service=service) + for name in dirs: + setntacl(lp, os.path.join(root, name), acl, domsid, + use_ntvfs=use_ntvfs, skip_invalid_chown=True, passdb=passdb, service=service) + + +def set_gpos_acl(sysvol, dnsdomain, domainsid, domaindn, samdb, lp, use_ntvfs, passdb): + """Set ACL on the sysvol/<dnsname>/Policies folder and the policy + folders beneath. + + :param sysvol: Physical path for the sysvol folder + :param dnsdomain: The DNS name of the domain + :param domainsid: The SID of the domain + :param domaindn: The DN of the domain (ie. DC=...) + :param samdb: An LDB object on the SAM db + :param lp: an LP object + """ + + # Set ACL for GPO root folder + root_policy_path = os.path.join(sysvol, dnsdomain, "Policies") + setntacl(lp, root_policy_path, POLICIES_ACL, str(domainsid), + use_ntvfs=use_ntvfs, skip_invalid_chown=True, passdb=passdb, service=SYSVOL_SERVICE) + + res = samdb.search(base="CN=Policies,CN=System,%s"%(domaindn), + attrs=["cn", "nTSecurityDescriptor"], + expression="", scope=ldb.SCOPE_ONELEVEL) + + for policy in res: + acl = ndr_unpack(security.descriptor, + str(policy["nTSecurityDescriptor"])).as_sddl() + policy_path = getpolicypath(sysvol, dnsdomain, str(policy["cn"])) + set_dir_acl(policy_path, dsacl2fsacl(acl, domainsid), lp, + str(domainsid), use_ntvfs, + passdb=passdb) + + +def setsysvolacl(samdb, netlogon, sysvol, uid, gid, domainsid, dnsdomain, + domaindn, lp, use_ntvfs): + """Set the ACL for the sysvol share and the subfolders + + :param samdb: An LDB object on the SAM db + :param netlogon: Physical path for the netlogon folder + :param sysvol: Physical path for the sysvol folder + :param uid: The UID of the "Administrator" user + :param gid: The GID of the "Domain adminstrators" group + :param domainsid: The SID of the domain + :param dnsdomain: The DNS name of the domain + :param domaindn: The DN of the domain (ie. DC=...) + """ + s4_passdb = None + + if not use_ntvfs: + # This will ensure that the smbd code we are running when setting ACLs + # is initialised with the smb.conf + s3conf = s3param.get_context() + s3conf.load(lp.configfile) + # ensure we are using the right samba_dsdb passdb backend, no matter what + s3conf.set("passdb backend", "samba_dsdb:%s" % samdb.url) + passdb.reload_static_pdb() + + # ensure that we init the samba_dsdb backend, so the domain sid is + # marked in secrets.tdb + s4_passdb = passdb.PDB(s3conf.get("passdb backend")) + + # now ensure everything matches correctly, to avoid wierd issues + if passdb.get_global_sam_sid() != domainsid: + raise ProvisioningError('SID as seen by smbd [%s] does not match SID as seen by the provision script [%s]!' % (passdb.get_global_sam_sid(), domainsid)) + + domain_info = s4_passdb.domain_info() + if domain_info["dom_sid"] != domainsid: + raise ProvisioningError('SID as seen by pdb_samba_dsdb [%s] does not match SID as seen by the provision script [%s]!' % (domain_info["dom_sid"], domainsid)) + + if domain_info["dns_domain"].upper() != dnsdomain.upper(): + raise ProvisioningError('Realm as seen by pdb_samba_dsdb [%s] does not match Realm as seen by the provision script [%s]!' % (domain_info["dns_domain"].upper(), dnsdomain.upper())) + + + try: + if use_ntvfs: + os.chown(sysvol, -1, gid) + except OSError: + canchown = False + else: + canchown = True + + # Set the SYSVOL_ACL on the sysvol folder and subfolder (first level) + setntacl(lp,sysvol, SYSVOL_ACL, str(domainsid), use_ntvfs=use_ntvfs, + skip_invalid_chown=True, passdb=s4_passdb, + service=SYSVOL_SERVICE) + for root, dirs, files in os.walk(sysvol, topdown=False): + for name in files: + if use_ntvfs and canchown: + os.chown(os.path.join(root, name), -1, gid) + setntacl(lp, os.path.join(root, name), SYSVOL_ACL, str(domainsid), + use_ntvfs=use_ntvfs, skip_invalid_chown=True, + passdb=s4_passdb, service=SYSVOL_SERVICE) + for name in dirs: + if use_ntvfs and canchown: + os.chown(os.path.join(root, name), -1, gid) + setntacl(lp, os.path.join(root, name), SYSVOL_ACL, str(domainsid), + use_ntvfs=use_ntvfs, skip_invalid_chown=True, + passdb=s4_passdb, service=SYSVOL_SERVICE) + + # Set acls on Policy folder and policies folders + set_gpos_acl(sysvol, dnsdomain, domainsid, domaindn, samdb, lp, use_ntvfs, passdb=s4_passdb) + +def acl_type(direct_db_access): + if direct_db_access: + return "DB" + else: + return "VFS" + +def check_dir_acl(path, acl, lp, domainsid, direct_db_access): + fsacl = getntacl(lp, path, direct_db_access=direct_db_access, service=SYSVOL_SERVICE) + fsacl_sddl = fsacl.as_sddl(domainsid) + if fsacl_sddl != acl: + raise ProvisioningError('%s ACL on GPO directory %s %s does not match expected value %s from GPO object' % (acl_type(direct_db_access), path, fsacl_sddl, acl)) + + for root, dirs, files in os.walk(path, topdown=False): + for name in files: + fsacl = getntacl(lp, os.path.join(root, name), + direct_db_access=direct_db_access, service=SYSVOL_SERVICE) + if fsacl is None: + raise ProvisioningError('%s ACL on GPO file %s %s not found!' % (acl_type(direct_db_access), os.path.join(root, name))) + fsacl_sddl = fsacl.as_sddl(domainsid) + if fsacl_sddl != acl: + raise ProvisioningError('%s ACL on GPO file %s %s does not match expected value %s from GPO object' % (acl_type(direct_db_access), os.path.join(root, name), fsacl_sddl, acl)) + + for name in dirs: + fsacl = getntacl(lp, os.path.join(root, name), + direct_db_access=direct_db_access, service=SYSVOL_SERVICE) + if fsacl is None: + raise ProvisioningError('%s ACL on GPO directory %s %s not found!' % (acl_type(direct_db_access), os.path.join(root, name))) + fsacl_sddl = fsacl.as_sddl(domainsid) + if fsacl_sddl != acl: + raise ProvisioningError('%s ACL on GPO directory %s %s does not match expected value %s from GPO object' % (acl_type(direct_db_access), os.path.join(root, name), fsacl_sddl, acl)) + + +def check_gpos_acl(sysvol, dnsdomain, domainsid, domaindn, samdb, lp, + direct_db_access): + """Set ACL on the sysvol/<dnsname>/Policies folder and the policy + folders beneath. + + :param sysvol: Physical path for the sysvol folder + :param dnsdomain: The DNS name of the domain + :param domainsid: The SID of the domain + :param domaindn: The DN of the domain (ie. DC=...) + :param samdb: An LDB object on the SAM db + :param lp: an LP object + """ + + # Set ACL for GPO root folder + root_policy_path = os.path.join(sysvol, dnsdomain, "Policies") + fsacl = getntacl(lp, root_policy_path, + direct_db_access=direct_db_access, service=SYSVOL_SERVICE) + if fsacl is None: + raise ProvisioningError('DB ACL on policy root %s %s not found!' % (acl_type(direct_db_access), root_policy_path)) + fsacl_sddl = fsacl.as_sddl(domainsid) + if fsacl_sddl != POLICIES_ACL: + raise ProvisioningError('%s ACL on policy root %s %s does not match expected value %s from provision' % (acl_type(direct_db_access), root_policy_path, fsacl_sddl, fsacl)) + res = samdb.search(base="CN=Policies,CN=System,%s"%(domaindn), + attrs=["cn", "nTSecurityDescriptor"], + expression="", scope=ldb.SCOPE_ONELEVEL) + + for policy in res: + acl = ndr_unpack(security.descriptor, + str(policy["nTSecurityDescriptor"])).as_sddl() + policy_path = getpolicypath(sysvol, dnsdomain, str(policy["cn"])) + check_dir_acl(policy_path, dsacl2fsacl(acl, domainsid), lp, + domainsid, direct_db_access) + + +def checksysvolacl(samdb, netlogon, sysvol, domainsid, dnsdomain, domaindn, + lp): + """Set the ACL for the sysvol share and the subfolders + + :param samdb: An LDB object on the SAM db + :param netlogon: Physical path for the netlogon folder + :param sysvol: Physical path for the sysvol folder + :param uid: The UID of the "Administrator" user + :param gid: The GID of the "Domain adminstrators" group + :param domainsid: The SID of the domain + :param dnsdomain: The DNS name of the domain + :param domaindn: The DN of the domain (ie. DC=...) + """ + + # This will ensure that the smbd code we are running when setting ACLs is initialised with the smb.conf + s3conf = s3param.get_context() + s3conf.load(lp.configfile) + # ensure we are using the right samba_dsdb passdb backend, no matter what + s3conf.set("passdb backend", "samba_dsdb:%s" % samdb.url) + # ensure that we init the samba_dsdb backend, so the domain sid is marked in secrets.tdb + s4_passdb = passdb.PDB(s3conf.get("passdb backend")) + + # now ensure everything matches correctly, to avoid wierd issues + if passdb.get_global_sam_sid() != domainsid: + raise ProvisioningError('SID as seen by smbd [%s] does not match SID as seen by the provision script [%s]!' % (passdb.get_global_sam_sid(), domainsid)) + + domain_info = s4_passdb.domain_info() + if domain_info["dom_sid"] != domainsid: + raise ProvisioningError('SID as seen by pdb_samba_dsdb [%s] does not match SID as seen by the provision script [%s]!' % (domain_info["dom_sid"], domainsid)) + + if domain_info["dns_domain"].upper() != dnsdomain.upper(): + raise ProvisioningError('Realm as seen by pdb_samba_dsdb [%s] does not match Realm as seen by the provision script [%s]!' % (domain_info["dns_domain"].upper(), dnsdomain.upper())) + + # Ensure we can read this directly, and via the smbd VFS + for direct_db_access in [True, False]: + # Check the SYSVOL_ACL on the sysvol folder and subfolder (first level) + for dir_path in [os.path.join(sysvol, dnsdomain), netlogon]: + fsacl = getntacl(lp, dir_path, direct_db_access=direct_db_access, service=SYSVOL_SERVICE) + if fsacl is None: + raise ProvisioningError('%s ACL on sysvol directory %s not found!' % (acl_type(direct_db_access), dir_path)) + fsacl_sddl = fsacl.as_sddl(domainsid) + if fsacl_sddl != SYSVOL_ACL: + raise ProvisioningError('%s ACL on sysvol directory %s %s does not match expected value %s from provision' % (acl_type(direct_db_access), dir_path, fsacl_sddl, SYSVOL_ACL)) + + # Check acls on Policy folder and policies folders + check_gpos_acl(sysvol, dnsdomain, domainsid, domaindn, samdb, lp, + direct_db_access) + + +def interface_ips_v4(lp): + """return only IPv4 IPs""" + ips = samba.interface_ips(lp, False) + ret = [] + for i in ips: + if i.find(':') == -1: + ret.append(i) + return ret + + +def interface_ips_v6(lp, linklocal=False): + """return only IPv6 IPs""" + ips = samba.interface_ips(lp, False) + ret = [] + for i in ips: + if i.find(':') != -1 and (linklocal or i.find('%') == -1): + ret.append(i) + return ret + + +def provision_fill(samdb, secrets_ldb, logger, names, paths, + domainsid, schema=None, + targetdir=None, samdb_fill=FILL_FULL, + hostip=None, hostip6=None, + next_rid=1000, dc_rid=None, adminpass=None, krbtgtpass=None, + domainguid=None, policyguid=None, policyguid_dc=None, + invocationid=None, machinepass=None, ntdsguid=None, + dns_backend=None, dnspass=None, + serverrole=None, dom_for_fun_level=None, + am_rodc=False, lp=None, use_ntvfs=False, skip_sysvolacl=False): + # create/adapt the group policy GUIDs + # Default GUID for default policy are described at + # "How Core Group Policy Works" + # http://technet.microsoft.com/en-us/library/cc784268%28WS.10%29.aspx + if policyguid is None: + policyguid = DEFAULT_POLICY_GUID + policyguid = policyguid.upper() + if policyguid_dc is None: + policyguid_dc = DEFAULT_DC_POLICY_GUID + policyguid_dc = policyguid_dc.upper() + + if invocationid is None: + invocationid = str(uuid.uuid4()) + + if krbtgtpass is None: + krbtgtpass = samba.generate_random_password(128, 255) + if machinepass is None: + machinepass = samba.generate_random_password(128, 255) + if dnspass is None: + dnspass = samba.generate_random_password(128, 255) + + samdb = fill_samdb(samdb, lp, names, logger=logger, + domainsid=domainsid, schema=schema, domainguid=domainguid, + policyguid=policyguid, policyguid_dc=policyguid_dc, + fill=samdb_fill, adminpass=adminpass, krbtgtpass=krbtgtpass, + invocationid=invocationid, machinepass=machinepass, + dns_backend=dns_backend, dnspass=dnspass, + ntdsguid=ntdsguid, serverrole=serverrole, + dom_for_fun_level=dom_for_fun_level, am_rodc=am_rodc, + next_rid=next_rid, dc_rid=dc_rid) + + if serverrole == "active directory domain controller": + + # Set up group policies (domain policy and domain controller + # policy) + create_default_gpo(paths.sysvol, names.dnsdomain, policyguid, + policyguid_dc) + if not skip_sysvolacl: + setsysvolacl(samdb, paths.netlogon, paths.sysvol, paths.root_uid, + paths.root_gid, domainsid, names.dnsdomain, + names.domaindn, lp, use_ntvfs) + else: + logger.info("Setting acl on sysvol skipped") + + secretsdb_self_join(secrets_ldb, domain=names.domain, + realm=names.realm, dnsdomain=names.dnsdomain, + netbiosname=names.netbiosname, domainsid=domainsid, + machinepass=machinepass, secure_channel_type=SEC_CHAN_BDC) + + # Now set up the right msDS-SupportedEncryptionTypes into the DB + # In future, this might be determined from some configuration + kerberos_enctypes = str(ENC_ALL_TYPES) + + try: + msg = ldb.Message(ldb.Dn(samdb, + samdb.searchone("distinguishedName", + expression="samAccountName=%s$" % names.netbiosname, + scope=ldb.SCOPE_SUBTREE))) + msg["msDS-SupportedEncryptionTypes"] = ldb.MessageElement( + elements=kerberos_enctypes, flags=ldb.FLAG_MOD_REPLACE, + name="msDS-SupportedEncryptionTypes") + samdb.modify(msg) + except ldb.LdbError, (enum, estr): + if enum != ldb.ERR_NO_SUCH_ATTRIBUTE: + # It might be that this attribute does not exist in this schema + raise + + setup_ad_dns(samdb, secrets_ldb, domainsid, names, paths, lp, logger, + hostip=hostip, hostip6=hostip6, dns_backend=dns_backend, + dnspass=dnspass, os_level=dom_for_fun_level, + targetdir=targetdir, site=DEFAULTSITE) + + domainguid = samdb.searchone(basedn=samdb.get_default_basedn(), + attribute="objectGUID") + assert isinstance(domainguid, str) + + lastProvisionUSNs = get_last_provision_usn(samdb) + maxUSN = get_max_usn(samdb, str(names.rootdn)) + if lastProvisionUSNs is not None: + update_provision_usn(samdb, 0, maxUSN, invocationid, 1) + else: + set_provision_usn(samdb, 0, maxUSN, invocationid) + + logger.info("Setting up sam.ldb rootDSE marking as synchronized") + setup_modify_ldif(samdb, setup_path("provision_rootdse_modify.ldif"), + { 'NTDSGUID' : names.ntdsguid }) + + # fix any dangling GUIDs from the provision + logger.info("Fixing provision GUIDs") + chk = dbcheck(samdb, samdb_schema=samdb, verbose=False, fix=True, yes=True, + quiet=True) + samdb.transaction_start() + try: + # a small number of GUIDs are missing because of ordering issues in the + # provision code + for schema_obj in ['CN=Domain', 'CN=Organizational-Person', 'CN=Contact', 'CN=inetOrgPerson']: + chk.check_database(DN="%s,%s" % (schema_obj, names.schemadn), + scope=ldb.SCOPE_BASE, + attrs=['defaultObjectCategory']) + chk.check_database(DN="CN=IP Security,CN=System,%s" % names.domaindn, + scope=ldb.SCOPE_ONELEVEL, + attrs=['ipsecOwnersReference', + 'ipsecFilterReference', + 'ipsecISAKMPReference', + 'ipsecNegotiationPolicyReference', + 'ipsecNFAReference']) + except: + samdb.transaction_cancel() + raise + else: + samdb.transaction_commit() + + +_ROLES_MAP = { + "ROLE_STANDALONE": "standalone server", + "ROLE_DOMAIN_MEMBER": "member server", + "ROLE_DOMAIN_BDC": "active directory domain controller", + "ROLE_DOMAIN_PDC": "active directory domain controller", + "dc": "active directory domain controller", + "member": "member server", + "domain controller": "active directory domain controller", + "active directory domain controller": "active directory domain controller", + "member server": "member server", + "standalone": "standalone server", + "standalone server": "standalone server", + } + + +def sanitize_server_role(role): + """Sanitize a server role name. + + :param role: Server role + :raise ValueError: If the role can not be interpreted + :return: Sanitized server role (one of "member server", + "active directory domain controller", "standalone server") + """ + try: + return _ROLES_MAP[role] + except KeyError: + raise ValueError(role) + + +def provision_fake_ypserver(logger, samdb, domaindn, netbiosname, nisdomain, + maxuid, maxgid): + """Create AD entries for the fake ypserver. + + This is needed for being able to manipulate posix attrs via ADUC. + """ + samdb.transaction_start() + try: + logger.info("Setting up fake yp server settings") + setup_add_ldif(samdb, setup_path("ypServ30.ldif"), { + "DOMAINDN": domaindn, + "NETBIOSNAME": netbiosname, + "NISDOMAIN": nisdomain, + }) + except: + samdb.transaction_cancel() + raise + else: + samdb.transaction_commit() + + +def provision(logger, session_info, credentials, smbconf=None, + targetdir=None, samdb_fill=FILL_FULL, realm=None, rootdn=None, + domaindn=None, schemadn=None, configdn=None, serverdn=None, + domain=None, hostname=None, hostip=None, hostip6=None, domainsid=None, + next_rid=1000, dc_rid=None, adminpass=None, ldapadminpass=None, + krbtgtpass=None, domainguid=None, policyguid=None, policyguid_dc=None, + dns_backend=None, dns_forwarder=None, dnspass=None, + invocationid=None, machinepass=None, ntdsguid=None, + root=None, nobody=None, users=None, backup=None, aci=None, + serverrole=None, dom_for_fun_level=None, backend_type=None, + sitename=None, ol_mmr_urls=None, ol_olc=None, slapd_path="/bin/false", + useeadb=False, am_rodc=False, lp=None, use_ntvfs=False, + use_rfc2307=False, maxuid=None, maxgid=None, skip_sysvolacl=True): + """Provision samba4 + + :note: caution, this wipes all existing data! + """ + + try: + serverrole = sanitize_server_role(serverrole) + except ValueError: + raise ProvisioningError('server role (%s) should be one of "active directory domain controller", "member server", "standalone server"' % serverrole) + + if ldapadminpass is None: + # Make a new, random password between Samba and it's LDAP server + ldapadminpass = samba.generate_random_password(128, 255) + + if backend_type is None: + backend_type = "ldb" + + if domainsid is None: + domainsid = security.random_sid() + else: + domainsid = security.dom_sid(domainsid) + + root_uid = findnss_uid([root or "root"]) + nobody_uid = findnss_uid([nobody or "nobody"]) + users_gid = findnss_gid([users or "users", 'users', 'other', 'staff']) + root_gid = pwd.getpwuid(root_uid).pw_gid + + try: + bind_gid = findnss_gid(["bind", "named"]) + except KeyError: + bind_gid = None + + if targetdir is not None: + smbconf = os.path.join(targetdir, "etc", "smb.conf") + elif smbconf is None: + smbconf = samba.param.default_path() + if not os.path.exists(os.path.dirname(smbconf)): + os.makedirs(os.path.dirname(smbconf)) + + server_services = [] + global_param = {} + if use_rfc2307: + global_param["idmap_ldb:use rfc2307"] = ["yes"] + + if dns_backend != "SAMBA_INTERNAL": + server_services.append("-dns") + else: + if dns_forwarder is not None: + global_param["dns forwarder"] = [dns_forwarder] + + if use_ntvfs: + server_services.append("+smb") + server_services.append("-s3fs") + global_param["dcerpc endpoint servers"] = ["+winreg", "+srvsvc"] + + if len(server_services) > 0: + global_param["server services"] = server_services + + # only install a new smb.conf if there isn't one there already + if os.path.exists(smbconf): + # if Samba Team members can't figure out the weird errors + # loading an empty smb.conf gives, then we need to be smarter. + # Pretend it just didn't exist --abartlet + f = open(smbconf, 'r') + try: + data = f.read().lstrip() + finally: + f.close() + if data is None or data == "": + make_smbconf(smbconf, hostname, domain, realm, + targetdir, serverrole=serverrole, + eadb=useeadb, use_ntvfs=use_ntvfs, + lp=lp, global_param=global_param) + else: + make_smbconf(smbconf, hostname, domain, realm, targetdir, + serverrole=serverrole, + eadb=useeadb, use_ntvfs=use_ntvfs, lp=lp, global_param=global_param) + + if lp is None: + lp = samba.param.LoadParm() + lp.load(smbconf) + names = guess_names(lp=lp, hostname=hostname, domain=domain, + dnsdomain=realm, serverrole=serverrole, domaindn=domaindn, + configdn=configdn, schemadn=schemadn, serverdn=serverdn, + sitename=sitename, rootdn=rootdn) + paths = provision_paths_from_lp(lp, names.dnsdomain) + + paths.bind_gid = bind_gid + paths.root_uid = root_uid; + paths.root_gid = root_gid + + if hostip is None: + logger.info("Looking up IPv4 addresses") + hostips = interface_ips_v4(lp) + if len(hostips) > 0: + hostip = hostips[0] + if len(hostips) > 1: + logger.warning("More than one IPv4 address found. Using %s", + hostip) + if hostip == "127.0.0.1": + hostip = None + if hostip is None: + logger.warning("No IPv4 address will be assigned") + + if hostip6 is None: + logger.info("Looking up IPv6 addresses") + hostips = interface_ips_v6(lp, linklocal=False) + if hostips: + hostip6 = hostips[0] + if len(hostips) > 1: + logger.warning("More than one IPv6 address found. Using %s", hostip6) + if hostip6 is None: + logger.warning("No IPv6 address will be assigned") + + names.hostip = hostip + names.hostip6 = hostip6 + + if serverrole is None: + serverrole = lp.get("server role") + + if not os.path.exists(paths.private_dir): + os.mkdir(paths.private_dir) + if not os.path.exists(os.path.join(paths.private_dir, "tls")): + os.mkdir(os.path.join(paths.private_dir, "tls")) + if not os.path.exists(paths.state_dir): + os.mkdir(paths.state_dir) + + if paths.sysvol and not os.path.exists(paths.sysvol): + os.makedirs(paths.sysvol, 0775) + + if not use_ntvfs and serverrole == "active directory domain controller": + s3conf = s3param.get_context() + s3conf.load(lp.configfile) + + if paths.sysvol is None: + raise MissingShareError("sysvol", paths.smbconf) + + file = tempfile.NamedTemporaryFile(dir=os.path.abspath(paths.sysvol)) + try: + try: + smbd.set_simple_acl(file.name, 0755, root_gid) + except Exception: + if not smbd.have_posix_acls(): + # This clue is only strictly correct for RPM and + # Debian-like Linux systems, but hopefully other users + # will get enough clue from it. + raise ProvisioningError("Samba was compiled without the posix ACL support that s3fs requires. Try installing libacl1-dev or libacl-devel, then re-run configure and make.") + + raise ProvisioningError("Your filesystem or build does not support posix ACLs, which s3fs requires. Try the mounting the filesystem with the 'acl' option.") + try: + smbd.chown(file.name, root_uid, root_gid) + except Exception: + raise ProvisioningError("Unable to chown a file on your filesystem. You may not be running provision as root.") + finally: + file.close() + + ldapi_url = "ldapi://%s" % urllib.quote(paths.s4_ldapi_path, safe="") + + schema = Schema(domainsid, invocationid=invocationid, + schemadn=names.schemadn) + + if backend_type == "ldb": + provision_backend = LDBBackend(backend_type, paths=paths, + lp=lp, credentials=credentials, + names=names, logger=logger) + elif backend_type == "existing": + # If support for this is ever added back, then the URI will need to be + # specified again + provision_backend = ExistingBackend(backend_type, paths=paths, + lp=lp, credentials=credentials, + names=names, logger=logger, + ldap_backend_forced_uri=None) + elif backend_type == "fedora-ds": + provision_backend = FDSBackend(backend_type, paths=paths, + lp=lp, credentials=credentials, + names=names, logger=logger, domainsid=domainsid, + schema=schema, hostname=hostname, ldapadminpass=ldapadminpass, + slapd_path=slapd_path, + root=root) + elif backend_type == "openldap": + provision_backend = OpenLDAPBackend(backend_type, paths=paths, + lp=lp, credentials=credentials, + names=names, logger=logger, domainsid=domainsid, + schema=schema, hostname=hostname, ldapadminpass=ldapadminpass, + slapd_path=slapd_path, ol_mmr_urls=ol_mmr_urls) + else: + raise ValueError("Unknown LDAP backend type selected") + + provision_backend.init() + provision_backend.start() + + # only install a new shares config db if there is none + if not os.path.exists(paths.shareconf): + logger.info("Setting up share.ldb") + share_ldb = Ldb(paths.shareconf, session_info=session_info, lp=lp) + share_ldb.load_ldif_file_add(setup_path("share.ldif")) + + logger.info("Setting up secrets.ldb") + secrets_ldb = setup_secretsdb(paths, + session_info=session_info, + backend_credentials=provision_backend.secrets_credentials, lp=lp) + + try: + logger.info("Setting up the registry") + setup_registry(paths.hklm, session_info, lp=lp) + + logger.info("Setting up the privileges database") + setup_privileges(paths.privilege, session_info, lp=lp) + + logger.info("Setting up idmap db") + idmap = setup_idmapdb(paths.idmapdb, session_info=session_info, lp=lp) + + setup_name_mappings(idmap, sid=str(domainsid), + root_uid=root_uid, nobody_uid=nobody_uid, + users_gid=users_gid, root_gid=root_gid) + + logger.info("Setting up SAM db") + samdb = setup_samdb(paths.samdb, session_info, + provision_backend, lp, names, logger=logger, + serverrole=serverrole, + schema=schema, fill=samdb_fill, am_rodc=am_rodc) + + if serverrole == "active directory domain controller": + if paths.netlogon is None: + raise MissingShareError("netlogon", paths.smbconf) + + if paths.sysvol is None: + raise MissingShareError("sysvol", paths.smbconf) + + if not os.path.isdir(paths.netlogon): + os.makedirs(paths.netlogon, 0755) + + if adminpass is None: + adminpass = samba.generate_random_password(12, 32) + adminpass_generated = True + else: + adminpass = unicode(adminpass, 'utf-8') + adminpass_generated = False + + if samdb_fill == FILL_FULL: + provision_fill(samdb, secrets_ldb, logger, names, paths, + schema=schema, targetdir=targetdir, samdb_fill=samdb_fill, + hostip=hostip, hostip6=hostip6, domainsid=domainsid, + next_rid=next_rid, dc_rid=dc_rid, adminpass=adminpass, + krbtgtpass=krbtgtpass, domainguid=domainguid, + policyguid=policyguid, policyguid_dc=policyguid_dc, + invocationid=invocationid, machinepass=machinepass, + ntdsguid=ntdsguid, dns_backend=dns_backend, + dnspass=dnspass, serverrole=serverrole, + dom_for_fun_level=dom_for_fun_level, am_rodc=am_rodc, + lp=lp, use_ntvfs=use_ntvfs, + skip_sysvolacl=skip_sysvolacl) + + create_krb5_conf(paths.krb5conf, + dnsdomain=names.dnsdomain, hostname=names.hostname, + realm=names.realm) + logger.info("A Kerberos configuration suitable for Samba 4 has been " + "generated at %s", paths.krb5conf) + + if serverrole == "active directory domain controller": + create_dns_update_list(lp, logger, paths) + + backend_result = provision_backend.post_setup() + provision_backend.shutdown() + + except: + secrets_ldb.transaction_cancel() + raise + + # Now commit the secrets.ldb to disk + secrets_ldb.transaction_commit() + + # the commit creates the dns.keytab, now chown it + dns_keytab_path = os.path.join(paths.private_dir, paths.dns_keytab) + if os.path.isfile(dns_keytab_path) and paths.bind_gid is not None: + try: + os.chmod(dns_keytab_path, 0640) + os.chown(dns_keytab_path, -1, paths.bind_gid) + except OSError: + if not os.environ.has_key('SAMBA_SELFTEST'): + logger.info("Failed to chown %s to bind gid %u", + dns_keytab_path, paths.bind_gid) + + result = ProvisionResult() + result.server_role = serverrole + result.domaindn = domaindn + result.paths = paths + result.names = names + result.lp = lp + result.samdb = samdb + result.idmap = idmap + result.domainsid = str(domainsid) + + if samdb_fill == FILL_FULL: + result.adminpass_generated = adminpass_generated + result.adminpass = adminpass + else: + result.adminpass_generated = False + result.adminpass = None + + result.backend_result = backend_result + + if use_rfc2307: + provision_fake_ypserver(logger=logger, samdb=samdb, + domaindn=names.domaindn, netbiosname=names.netbiosname, + nisdomain=names.domain.lower(), maxuid=maxuid, maxgid=maxgid) + + return result + + +def provision_become_dc(smbconf=None, targetdir=None, + realm=None, rootdn=None, domaindn=None, schemadn=None, configdn=None, + serverdn=None, domain=None, hostname=None, domainsid=None, + adminpass=None, krbtgtpass=None, domainguid=None, policyguid=None, + policyguid_dc=None, invocationid=None, machinepass=None, dnspass=None, + dns_backend=None, root=None, nobody=None, users=None, + backup=None, serverrole=None, ldap_backend=None, + ldap_backend_type=None, sitename=None, debuglevel=1, use_ntvfs=False): + + logger = logging.getLogger("provision") + samba.set_debug_level(debuglevel) + + res = provision(logger, system_session(), None, + smbconf=smbconf, targetdir=targetdir, samdb_fill=FILL_DRS, + realm=realm, rootdn=rootdn, domaindn=domaindn, schemadn=schemadn, + configdn=configdn, serverdn=serverdn, domain=domain, + hostname=hostname, hostip=None, domainsid=domainsid, + machinepass=machinepass, + serverrole="active directory domain controller", + sitename=sitename, dns_backend=dns_backend, dnspass=dnspass, + use_ntvfs=use_ntvfs) + res.lp.set("debuglevel", str(debuglevel)) + return res + + +def create_krb5_conf(path, dnsdomain, hostname, realm): + """Write out a file containing zone statements suitable for inclusion in a + named.conf file (including GSS-TSIG configuration). + + :param path: Path of the new named.conf file. + :param dnsdomain: DNS Domain name + :param hostname: Local hostname + :param realm: Realm name + """ + setup_file(setup_path("krb5.conf"), path, { + "DNSDOMAIN": dnsdomain, + "HOSTNAME": hostname, + "REALM": realm, + }) + + +class ProvisioningError(Exception): + """A generic provision error.""" + + def __init__(self, value): + self.value = value + + def __str__(self): + return "ProvisioningError: " + self.value + + +class InvalidNetbiosName(Exception): + """A specified name was not a valid NetBIOS name.""" + + def __init__(self, name): + super(InvalidNetbiosName, self).__init__( + "The name '%r' is not a valid NetBIOS name" % name) + + +class MissingShareError(ProvisioningError): + + def __init__(self, name, smbconf): + super(MissingShareError, self).__init__( + "Existing smb.conf does not have a [%s] share, but you are " + "configuring a DC. Please remove %s or add the share manually." % + (name, smbconf)) diff --git a/python/samba/provision/backend.py b/python/samba/provision/backend.py new file mode 100644 index 00000000000..f88b0db89c5 --- /dev/null +++ b/python/samba/provision/backend.py @@ -0,0 +1,840 @@ +# +# Unix SMB/CIFS implementation. +# backend code for provisioning a Samba4 server + +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2008 +# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2008-2009 +# Copyright (C) Oliver Liebel <oliver@itc.li> 2008-2009 +# +# Based on the original in EJS: +# Copyright (C) Andrew Tridgell <tridge@samba.org> 2005 +# +# 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/>. +# + +"""Functions for setting up a Samba configuration (LDB and LDAP backends).""" + +from base64 import b64encode +import errno +import ldb +import os +import sys +import uuid +import time +import shutil +import subprocess +import urllib + +from ldb import SCOPE_BASE, SCOPE_ONELEVEL, LdbError, timestring + +from samba import Ldb, read_and_sub_file, setup_file +from samba.credentials import Credentials, DONT_USE_KERBEROS +from samba.schema import Schema + + +class SlapdAlreadyRunning(Exception): + + def __init__(self, uri): + self.ldapi_uri = uri + super(SlapdAlreadyRunning, self).__init__("Another slapd Instance " + "seems already running on this host, listening to %s." % + self.ldapi_uri) + + +class BackendResult(object): + + def report_logger(self, logger): + """Rerport this result to a particular logger. + + """ + raise NotImplementedError(self.report_logger) + + +class LDAPBackendResult(BackendResult): + + def __init__(self, credentials, slapd_command_escaped, ldapdir): + self.credentials = credentials + self.slapd_command_escaped = slapd_command_escaped + self.ldapdir = ldapdir + + def report_logger(self, logger): + if self.credentials.get_bind_dn() is not None: + logger.info("LDAP Backend Admin DN: %s" % + self.credentials.get_bind_dn()) + else: + logger.info("LDAP Admin User: %s" % + self.credentials.get_username()) + + if self.slapd_command_escaped is not None: + # now display slapd_command_file.txt to show how slapd must be + # started next time + logger.info( + "Use later the following commandline to start slapd, then Samba:") + logger.info(self.slapd_command_escaped) + logger.info( + "This slapd-Commandline is also stored under: %s/ldap_backend_startup.sh", + self.ldapdir) + + +class ProvisionBackend(object): + + def __init__(self, backend_type, paths=None, lp=None, + credentials=None, names=None, logger=None): + """Provision a backend for samba4""" + self.paths = paths + self.lp = lp + self.credentials = credentials + self.names = names + self.logger = logger + + self.type = backend_type + + # Set a default - the code for "existing" below replaces this + self.ldap_backend_type = backend_type + + def init(self): + """Initialize the backend.""" + raise NotImplementedError(self.init) + + def start(self): + """Start the backend.""" + raise NotImplementedError(self.start) + + def shutdown(self): + """Shutdown the backend.""" + raise NotImplementedError(self.shutdown) + + def post_setup(self): + """Post setup. + + :return: A BackendResult or None + """ + raise NotImplementedError(self.post_setup) + + +class LDBBackend(ProvisionBackend): + + def init(self): + self.credentials = None + self.secrets_credentials = None + + # Wipe the old sam.ldb databases away + shutil.rmtree(self.paths.samdb + ".d", True) + + def start(self): + pass + + def shutdown(self): + pass + + def post_setup(self): + pass + + +class ExistingBackend(ProvisionBackend): + + def __init__(self, backend_type, paths=None, lp=None, + credentials=None, names=None, logger=None, ldapi_uri=None): + + super(ExistingBackend, self).__init__(backend_type=backend_type, + paths=paths, lp=lp, + credentials=credentials, names=names, logger=logger, + ldap_backend_forced_uri=ldapi_uri) + + def init(self): + # Check to see that this 'existing' LDAP backend in fact exists + ldapi_db = Ldb(self.ldapi_uri, credentials=self.credentials) + ldapi_db.search(base="", scope=SCOPE_BASE, + expression="(objectClass=OpenLDAProotDSE)") + + # If we have got here, then we must have a valid connection to the LDAP + # server, with valid credentials supplied This caused them to be set + # into the long-term database later in the script. + self.secrets_credentials = self.credentials + + # For now, assume existing backends at least emulate OpenLDAP + self.ldap_backend_type = "openldap" + + +class LDAPBackend(ProvisionBackend): + + def __init__(self, backend_type, paths=None, lp=None, + credentials=None, names=None, logger=None, domainsid=None, + schema=None, hostname=None, ldapadminpass=None, + slapd_path=None, ldap_backend_extra_port=None, + ldap_backend_forced_uri=None, ldap_dryrun_mode=True): + + super(LDAPBackend, self).__init__(backend_type=backend_type, + paths=paths, lp=lp, + credentials=credentials, names=names, logger=logger) + + self.domainsid = domainsid + self.schema = schema + self.hostname = hostname + + self.ldapdir = os.path.join(paths.private_dir, "ldap") + self.ldapadminpass = ldapadminpass + + self.slapd_path = slapd_path + self.slapd_command = None + self.slapd_command_escaped = None + self.slapd_pid = os.path.join(self.ldapdir, "slapd.pid") + + self.ldap_backend_extra_port = ldap_backend_extra_port + self.ldap_dryrun_mode = ldap_dryrun_mode + + if ldap_backend_forced_uri is not None: + self.ldap_uri = ldap_backend_forced_uri + else: + self.ldap_uri = "ldapi://%s" % urllib.quote( + os.path.join(self.ldapdir, "ldapi"), safe="") + + if not os.path.exists(self.ldapdir): + os.mkdir(self.ldapdir) + + def init(self): + from samba.provision import ProvisioningError + # we will shortly start slapd with ldapi for final provisioning. first + # check with ldapsearch -> rootDSE via self.ldap_uri if another + # instance of slapd is already running + try: + ldapi_db = Ldb(self.ldap_uri) + ldapi_db.search(base="", scope=SCOPE_BASE, + expression="(objectClass=OpenLDAProotDSE)") + try: + f = open(self.slapd_pid, "r") + except IOError, err: + if err != errno.ENOENT: + raise + else: + try: + p = f.read() + finally: + f.close() + self.logger.info("Check for slapd process with PID: %s and terminate it manually." % p) + raise SlapdAlreadyRunning(self.ldap_uri) + except LdbError: + # XXX: We should never be catching all Ldb errors + pass + + # Try to print helpful messages when the user has not specified the + # path to slapd + if self.slapd_path is None: + raise ProvisioningError("Warning: LDAP-Backend must be setup with path to slapd, e.g. --slapd-path=\"/usr/local/libexec/slapd\"!") + if not os.path.exists(self.slapd_path): + self.logger.warning("Path (%s) to slapd does not exist!", + self.slapd_path) + + if not os.path.isdir(self.ldapdir): + os.makedirs(self.ldapdir, 0700) + + # Put the LDIF of the schema into a database so we can search on + # it to generate schema-dependent configurations in Fedora DS and + # OpenLDAP + schemadb_path = os.path.join(self.ldapdir, "schema-tmp.ldb") + try: + os.unlink(schemadb_path) + except OSError: + pass + + self.schema.write_to_tmp_ldb(schemadb_path) + + self.credentials = Credentials() + self.credentials.guess(self.lp) + # Kerberos to an ldapi:// backend makes no sense + self.credentials.set_kerberos_state(DONT_USE_KERBEROS) + self.credentials.set_password(self.ldapadminpass) + + self.secrets_credentials = Credentials() + self.secrets_credentials.guess(self.lp) + # Kerberos to an ldapi:// backend makes no sense + self.secrets_credentials.set_kerberos_state(DONT_USE_KERBEROS) + self.secrets_credentials.set_username("samba-admin") + self.secrets_credentials.set_password(self.ldapadminpass) + + self.provision() + + def provision(self): + pass + + def start(self): + from samba.provision import ProvisioningError + self.slapd_command_escaped = "\'" + "\' \'".join(self.slapd_command) + "\'" + f = open(os.path.join(self.ldapdir, "ldap_backend_startup.sh"), 'w') + try: + f.write("#!/bin/sh\n" + self.slapd_command_escaped + "\n") + finally: + f.close() + + # Now start the slapd, so we can provision onto it. We keep the + # subprocess context around, to kill this off at the successful + # end of the script + self.slapd = subprocess.Popen(self.slapd_provision_command, + close_fds=True, shell=False) + + count = 0 + while self.slapd.poll() is None: + # Wait until the socket appears + try: + ldapi_db = Ldb(self.ldap_uri, lp=self.lp, credentials=self.credentials) + ldapi_db.search(base="", scope=SCOPE_BASE, + expression="(objectClass=OpenLDAProotDSE)") + # If we have got here, then we must have a valid connection to + # the LDAP server! + return + except LdbError: + time.sleep(1) + count = count + 1 + + if count > 15: + self.logger.error("Could not connect to slapd started with: %s" % "\'" + "\' \'".join(self.slapd_provision_command) + "\'") + raise ProvisioningError("slapd never accepted a connection within 15 seconds of starting") + + self.logger.error("Could not start slapd with: %s" % "\'" + "\' \'".join(self.slapd_provision_command) + "\'") + raise ProvisioningError("slapd died before we could make a connection to it") + + def shutdown(self): + # if an LDAP backend is in use, terminate slapd after final provision + # and check its proper termination + if self.slapd.poll() is None: + # Kill the slapd + if getattr(self.slapd, "terminate", None) is not None: + self.slapd.terminate() + else: + # Older python versions don't have .terminate() + import signal + os.kill(self.slapd.pid, signal.SIGTERM) + + # and now wait for it to die + self.slapd.communicate() + + def post_setup(self): + return LDAPBackendResult(self.credentials, self.slapd_command_escaped, + self.ldapdir) + + +class OpenLDAPBackend(LDAPBackend): + + def __init__(self, backend_type, paths=None, lp=None, + credentials=None, names=None, logger=None, domainsid=None, + schema=None, hostname=None, ldapadminpass=None, slapd_path=None, + ldap_backend_extra_port=None, ldap_dryrun_mode=True, + ol_mmr_urls=None, nosync=False, ldap_backend_forced_uri=None): + from samba.provision import setup_path + super(OpenLDAPBackend, self).__init__( backend_type=backend_type, + paths=paths, lp=lp, + credentials=credentials, names=names, logger=logger, + domainsid=domainsid, schema=schema, hostname=hostname, + ldapadminpass=ldapadminpass, slapd_path=slapd_path, + ldap_backend_extra_port=ldap_backend_extra_port, + ldap_backend_forced_uri=ldap_backend_forced_uri, + ldap_dryrun_mode=ldap_dryrun_mode) + + self.ol_mmr_urls = ol_mmr_urls + self.nosync = nosync + + self.slapdconf = os.path.join(self.ldapdir, "slapd.conf") + self.modulesconf = os.path.join(self.ldapdir, "modules.conf") + self.memberofconf = os.path.join(self.ldapdir, "memberof.conf") + self.olmmrserveridsconf = os.path.join(self.ldapdir, "mmr_serverids.conf") + self.olmmrsyncreplconf = os.path.join(self.ldapdir, "mmr_syncrepl.conf") + self.olcdir = os.path.join(self.ldapdir, "slapd.d") + self.olcseedldif = os.path.join(self.ldapdir, "olc_seed.ldif") + + self.schema = Schema(self.domainsid, + schemadn=self.names.schemadn, files=[ + setup_path("schema_samba4.ldif")]) + + def setup_db_config(self, dbdir): + """Setup a Berkeley database. + + :param dbdir: Database directory. + """ + from samba.provision import setup_path + if not os.path.isdir(os.path.join(dbdir, "bdb-logs")): + os.makedirs(os.path.join(dbdir, "bdb-logs"), 0700) + if not os.path.isdir(os.path.join(dbdir, "tmp")): + os.makedirs(os.path.join(dbdir, "tmp"), 0700) + + setup_file(setup_path("DB_CONFIG"), + os.path.join(dbdir, "DB_CONFIG"), {"LDAPDBDIR": dbdir}) + + def provision(self): + from samba.provision import ProvisioningError, setup_path + # Wipe the directories so we can start + shutil.rmtree(os.path.join(self.ldapdir, "db"), True) + + # Allow the test scripts to turn off fsync() for OpenLDAP as for TDB + # and LDB + nosync_config = "" + if self.nosync: + nosync_config = "dbnosync" + + lnkattr = self.schema.linked_attributes() + refint_attributes = "" + memberof_config = "# Generated from Samba4 schema\n" + for att in lnkattr.keys(): + if lnkattr[att] is not None: + refint_attributes = refint_attributes + " " + att + + memberof_config += read_and_sub_file( + setup_path("memberof.conf"), { + "MEMBER_ATTR": att, + "MEMBEROF_ATTR" : lnkattr[att] }) + + refint_config = read_and_sub_file(setup_path("refint.conf"), + { "LINK_ATTRS" : refint_attributes}) + + attrs = ["linkID", "lDAPDisplayName"] + res = self.schema.ldb.search(expression="(&(objectclass=attributeSchema)(searchFlags:1.2.840.113556.1.4.803:=1))", base=self.names.schemadn, scope=SCOPE_ONELEVEL, attrs=attrs) + index_config = "" + for i in range (0, len(res)): + index_attr = res[i]["lDAPDisplayName"][0] + if index_attr == "objectGUID": + index_attr = "entryUUID" + + index_config += "index " + index_attr + " eq\n" + + # generate serverids, ldap-urls and syncrepl-blocks for mmr hosts + mmr_on_config = "" + mmr_replicator_acl = "" + mmr_serverids_config = "" + mmr_syncrepl_schema_config = "" + mmr_syncrepl_config_config = "" + mmr_syncrepl_user_config = "" + + if self.ol_mmr_urls is not None: + # For now, make these equal + mmr_pass = self.ldapadminpass + + url_list = filter(None,self.ol_mmr_urls.split(',')) + for url in url_list: + self.logger.info("Using LDAP-URL: "+url) + if len(url_list) == 1: + raise ProvisioningError("At least 2 LDAP-URLs needed for MMR!") + + mmr_on_config = "MirrorMode On" + mmr_replicator_acl = " by dn=cn=replicator,cn=samba read" + serverid = 0 + for url in url_list: + serverid = serverid + 1 + mmr_serverids_config += read_and_sub_file( + setup_path("mmr_serverids.conf"), { + "SERVERID": str(serverid), + "LDAPSERVER": url }) + rid = serverid * 10 + rid = rid + 1 + mmr_syncrepl_schema_config += read_and_sub_file( + setup_path("mmr_syncrepl.conf"), { + "RID" : str(rid), + "MMRDN": self.names.schemadn, + "LDAPSERVER" : url, + "MMR_PASSWORD": mmr_pass}) + + rid = rid + 1 + mmr_syncrepl_config_config += read_and_sub_file( + setup_path("mmr_syncrepl.conf"), { + "RID" : str(rid), + "MMRDN": self.names.configdn, + "LDAPSERVER" : url, + "MMR_PASSWORD": mmr_pass}) + + rid = rid + 1 + mmr_syncrepl_user_config += read_and_sub_file( + setup_path("mmr_syncrepl.conf"), { + "RID" : str(rid), + "MMRDN": self.names.domaindn, + "LDAPSERVER" : url, + "MMR_PASSWORD": mmr_pass }) + # OpenLDAP cn=config initialisation + olc_syncrepl_config = "" + olc_mmr_config = "" + # if mmr = yes, generate cn=config-replication directives + # and olc_seed.lif for the other mmr-servers + if self.ol_mmr_urls is not None: + serverid = 0 + olc_serverids_config = "" + olc_syncrepl_seed_config = "" + olc_mmr_config += read_and_sub_file( + setup_path("olc_mmr.conf"), {}) + rid = 500 + for url in url_list: + serverid = serverid + 1 + olc_serverids_config += read_and_sub_file( + setup_path("olc_serverid.conf"), { + "SERVERID" : str(serverid), "LDAPSERVER" : url }) + + rid = rid + 1 + olc_syncrepl_config += read_and_sub_file( + setup_path("olc_syncrepl.conf"), { + "RID" : str(rid), "LDAPSERVER" : url, + "MMR_PASSWORD": mmr_pass}) + + olc_syncrepl_seed_config += read_and_sub_file( + setup_path("olc_syncrepl_seed.conf"), { + "RID" : str(rid), "LDAPSERVER" : url}) + + setup_file(setup_path("olc_seed.ldif"), self.olcseedldif, + {"OLC_SERVER_ID_CONF": olc_serverids_config, + "OLC_PW": self.ldapadminpass, + "OLC_SYNCREPL_CONF": olc_syncrepl_seed_config}) + # end olc + + setup_file(setup_path("slapd.conf"), self.slapdconf, + {"DNSDOMAIN": self.names.dnsdomain, + "LDAPDIR": self.ldapdir, + "DOMAINDN": self.names.domaindn, + "CONFIGDN": self.names.configdn, + "SCHEMADN": self.names.schemadn, + "MEMBEROF_CONFIG": memberof_config, + "MIRRORMODE": mmr_on_config, + "REPLICATOR_ACL": mmr_replicator_acl, + "MMR_SERVERIDS_CONFIG": mmr_serverids_config, + "MMR_SYNCREPL_SCHEMA_CONFIG": mmr_syncrepl_schema_config, + "MMR_SYNCREPL_CONFIG_CONFIG": mmr_syncrepl_config_config, + "MMR_SYNCREPL_USER_CONFIG": mmr_syncrepl_user_config, + "OLC_SYNCREPL_CONFIG": olc_syncrepl_config, + "OLC_MMR_CONFIG": olc_mmr_config, + "REFINT_CONFIG": refint_config, + "INDEX_CONFIG": index_config, + "NOSYNC": nosync_config}) + + self.setup_db_config(os.path.join(self.ldapdir, "db", "user")) + self.setup_db_config(os.path.join(self.ldapdir, "db", "config")) + self.setup_db_config(os.path.join(self.ldapdir, "db", "schema")) + + if not os.path.exists(os.path.join(self.ldapdir, "db", "samba", "cn=samba")): + os.makedirs(os.path.join(self.ldapdir, "db", "samba", "cn=samba"), 0700) + + setup_file(setup_path("cn=samba.ldif"), + os.path.join(self.ldapdir, "db", "samba", "cn=samba.ldif"), + { "UUID": str(uuid.uuid4()), + "LDAPTIME": timestring(int(time.time()))} ) + setup_file(setup_path("cn=samba-admin.ldif"), + os.path.join(self.ldapdir, "db", "samba", "cn=samba", "cn=samba-admin.ldif"), + {"LDAPADMINPASS_B64": b64encode(self.ldapadminpass), + "UUID": str(uuid.uuid4()), + "LDAPTIME": timestring(int(time.time()))} ) + + if self.ol_mmr_urls is not None: + setup_file(setup_path("cn=replicator.ldif"), + os.path.join(self.ldapdir, "db", "samba", "cn=samba", "cn=replicator.ldif"), + {"MMR_PASSWORD_B64": b64encode(mmr_pass), + "UUID": str(uuid.uuid4()), + "LDAPTIME": timestring(int(time.time()))} ) + + mapping = "schema-map-openldap-2.3" + backend_schema = "backend-schema.schema" + + f = open(setup_path(mapping), 'r') + try: + backend_schema_data = self.schema.convert_to_openldap( + "openldap", f.read()) + finally: + f.close() + assert backend_schema_data is not None + f = open(os.path.join(self.ldapdir, backend_schema), 'w') + try: + f.write(backend_schema_data) + finally: + f.close() + + # now we generate the needed strings to start slapd automatically, + if self.ldap_backend_extra_port is not None: + # When we use MMR, we can't use 0.0.0.0 as it uses the name + # specified there as part of it's clue as to it's own name, + # and not to replicate to itself + if self.ol_mmr_urls is None: + server_port_string = "ldap://0.0.0.0:%d" % self.ldap_backend_extra_port + else: + server_port_string = "ldap://%s.%s:%d" (self.names.hostname, + self.names.dnsdomain, self.ldap_backend_extra_port) + else: + server_port_string = "" + + # Prepare the 'result' information - the commands to return in + # particular + self.slapd_provision_command = [self.slapd_path, "-F" + self.olcdir, + "-h"] + + # copy this command so we have two version, one with -d0 and only + # ldapi (or the forced ldap_uri), and one with all the listen commands + self.slapd_command = list(self.slapd_provision_command) + + self.slapd_provision_command.extend([self.ldap_uri, "-d0"]) + + uris = self.ldap_uri + if server_port_string is not "": + uris = uris + " " + server_port_string + + self.slapd_command.append(uris) + + # Set the username - done here because Fedora DS still uses the admin + # DN and simple bind + self.credentials.set_username("samba-admin") + + # Wipe the old sam.ldb databases away + shutil.rmtree(self.olcdir, True) + os.makedirs(self.olcdir, 0770) + + # If we were just looking for crashes up to this point, it's a + # good time to exit before we realise we don't have OpenLDAP on + # this system + if self.ldap_dryrun_mode: + sys.exit(0) + + slapd_cmd = [self.slapd_path, "-Ttest", "-n", "0", "-f", + self.slapdconf, "-F", self.olcdir] + retcode = subprocess.call(slapd_cmd, close_fds=True, shell=False) + + if retcode != 0: + self.logger.error("conversion from slapd.conf to cn=config failed slapd started with: %s" % "\'" + "\' \'".join(slapd_cmd) + "\'") + raise ProvisioningError("conversion from slapd.conf to cn=config failed") + + if not os.path.exists(os.path.join(self.olcdir, "cn=config.ldif")): + raise ProvisioningError("conversion from slapd.conf to cn=config failed") + + # Don't confuse the admin by leaving the slapd.conf around + os.remove(self.slapdconf) + + +class FDSBackend(LDAPBackend): + + def __init__(self, backend_type, paths=None, lp=None, + credentials=None, names=None, logger=None, domainsid=None, + schema=None, hostname=None, ldapadminpass=None, slapd_path=None, + ldap_backend_extra_port=None, ldap_dryrun_mode=True, root=None, + setup_ds_path=None): + + from samba.provision import setup_path + + super(FDSBackend, self).__init__(backend_type=backend_type, + paths=paths, lp=lp, + credentials=credentials, names=names, logger=logger, + domainsid=domainsid, schema=schema, hostname=hostname, + ldapadminpass=ldapadminpass, slapd_path=slapd_path, + ldap_backend_extra_port=ldap_backend_extra_port, + ldap_backend_forced_uri=ldap_backend_forced_uri, + ldap_dryrun_mode=ldap_dryrun_mode) + + self.root = root + self.setup_ds_path = setup_ds_path + self.ldap_instance = self.names.netbiosname.lower() + + self.sambadn = "CN=Samba" + + self.fedoradsinf = os.path.join(self.ldapdir, "fedorads.inf") + self.partitions_ldif = os.path.join(self.ldapdir, + "fedorads-partitions.ldif") + self.sasl_ldif = os.path.join(self.ldapdir, "fedorads-sasl.ldif") + self.dna_ldif = os.path.join(self.ldapdir, "fedorads-dna.ldif") + self.pam_ldif = os.path.join(self.ldapdir, "fedorads-pam.ldif") + self.refint_ldif = os.path.join(self.ldapdir, "fedorads-refint.ldif") + self.linked_attrs_ldif = os.path.join(self.ldapdir, + "fedorads-linked-attributes.ldif") + self.index_ldif = os.path.join(self.ldapdir, "fedorads-index.ldif") + self.samba_ldif = os.path.join(self.ldapdir, "fedorads-samba.ldif") + + self.samba3_schema = setup_path( + "../../examples/LDAP/samba.schema") + self.samba3_ldif = os.path.join(self.ldapdir, "samba3.ldif") + + self.retcode = subprocess.call(["bin/oLschema2ldif", + "-I", self.samba3_schema, + "-O", self.samba3_ldif, + "-b", self.names.domaindn], + close_fds=True, shell=False) + + if self.retcode != 0: + raise Exception("Unable to convert Samba 3 schema.") + + self.schema = Schema( + self.domainsid, + schemadn=self.names.schemadn, + files=[setup_path("schema_samba4.ldif"), self.samba3_ldif], + additional_prefixmap=["1000:1.3.6.1.4.1.7165.2.1", + "1001:1.3.6.1.4.1.7165.2.2"]) + + def provision(self): + from samba.provision import ProvisioningError, setup_path + if self.ldap_backend_extra_port is not None: + serverport = "ServerPort=%d" % self.ldap_backend_extra_port + else: + serverport = "" + + setup_file(setup_path("fedorads.inf"), self.fedoradsinf, + {"ROOT": self.root, + "HOSTNAME": self.hostname, + "DNSDOMAIN": self.names.dnsdomain, + "LDAPDIR": self.ldapdir, + "DOMAINDN": self.names.domaindn, + "LDAP_INSTANCE": self.ldap_instance, + "LDAPMANAGERDN": self.names.ldapmanagerdn, + "LDAPMANAGERPASS": self.ldapadminpass, + "SERVERPORT": serverport}) + + setup_file(setup_path("fedorads-partitions.ldif"), + self.partitions_ldif, + {"CONFIGDN": self.names.configdn, + "SCHEMADN": self.names.schemadn, + "SAMBADN": self.sambadn, + }) + + setup_file(setup_path("fedorads-sasl.ldif"), self.sasl_ldif, + {"SAMBADN": self.sambadn, + }) + + setup_file(setup_path("fedorads-dna.ldif"), self.dna_ldif, + {"DOMAINDN": self.names.domaindn, + "SAMBADN": self.sambadn, + "DOMAINSID": str(self.domainsid), + }) + + setup_file(setup_path("fedorads-pam.ldif"), self.pam_ldif) + + lnkattr = self.schema.linked_attributes() + + f = open(setup_path("fedorads-refint-delete.ldif"), 'r') + try: + refint_config = f.read() + finally: + f.close() + memberof_config = "" + index_config = "" + argnum = 3 + + for attr in lnkattr.keys(): + if lnkattr[attr] is not None: + refint_config += read_and_sub_file( + setup_path("fedorads-refint-add.ldif"), + { "ARG_NUMBER" : str(argnum), + "LINK_ATTR" : attr }) + memberof_config += read_and_sub_file( + setup_path("fedorads-linked-attributes.ldif"), + { "MEMBER_ATTR" : attr, + "MEMBEROF_ATTR" : lnkattr[attr] }) + index_config += read_and_sub_file( + setup_path("fedorads-index.ldif"), { "ATTR" : attr }) + argnum += 1 + + f = open(self.refint_ldif, 'w') + try: + f.write(refint_config) + finally: + f.close() + f = open(self.linked_attrs_ldif, 'w') + try: + f.write(memberof_config) + finally: + f.close() + + attrs = ["lDAPDisplayName"] + res = self.schema.ldb.search(expression="(&(objectclass=attributeSchema)(searchFlags:1.2.840.113556.1.4.803:=1))", base=self.names.schemadn, scope=SCOPE_ONELEVEL, attrs=attrs) + + for i in range (0, len(res)): + attr = res[i]["lDAPDisplayName"][0] + + if attr == "objectGUID": + attr = "nsUniqueId" + + index_config += read_and_sub_file( + setup_path("fedorads-index.ldif"), { "ATTR" : attr }) + + f = open(self.index_ldif, 'w') + try: + f.write(index_config) + finally: + f.close() + + setup_file(setup_path("fedorads-samba.ldif"), self.samba_ldif, { + "SAMBADN": self.sambadn, + "LDAPADMINPASS": self.ldapadminpass + }) + + mapping = "schema-map-fedora-ds-1.0" + backend_schema = "99_ad.ldif" + + # Build a schema file in Fedora DS format + f = open(setup_path(mapping), 'r') + try: + backend_schema_data = self.schema.convert_to_openldap("fedora-ds", + f.read()) + finally: + f.close() + assert backend_schema_data is not None + f = open(os.path.join(self.ldapdir, backend_schema), 'w') + try: + f.write(backend_schema_data) + finally: + f.close() + + self.credentials.set_bind_dn(self.names.ldapmanagerdn) + + # Destory the target directory, or else setup-ds.pl will complain + fedora_ds_dir = os.path.join(self.ldapdir, + "slapd-" + self.ldap_instance) + shutil.rmtree(fedora_ds_dir, True) + + self.slapd_provision_command = [self.slapd_path, "-D", fedora_ds_dir, + "-i", self.slapd_pid] + # In the 'provision' command line, stay in the foreground so we can + # easily kill it + self.slapd_provision_command.append("-d0") + + #the command for the final run is the normal script + self.slapd_command = [os.path.join(self.ldapdir, + "slapd-" + self.ldap_instance, "start-slapd")] + + # If we were just looking for crashes up to this point, it's a + # good time to exit before we realise we don't have Fedora DS on + if self.ldap_dryrun_mode: + sys.exit(0) + + # Try to print helpful messages when the user has not specified the + # path to the setup-ds tool + if self.setup_ds_path is None: + raise ProvisioningError("Fedora DS LDAP-Backend must be setup with path to setup-ds, e.g. --setup-ds-path=\"/usr/sbin/setup-ds.pl\"!") + if not os.path.exists(self.setup_ds_path): + self.logger.warning("Path (%s) to slapd does not exist!", + self.setup_ds_path) + + # Run the Fedora DS setup utility + retcode = subprocess.call([self.setup_ds_path, "--silent", "--file", + self.fedoradsinf], close_fds=True, shell=False) + if retcode != 0: + raise ProvisioningError("setup-ds failed") + + # Load samba-admin + retcode = subprocess.call([ + os.path.join(self.ldapdir, "slapd-" + self.ldap_instance, "ldif2db"), "-s", self.sambadn, "-i", self.samba_ldif], + close_fds=True, shell=False) + if retcode != 0: + raise ProvisioningError("ldif2db failed") + + def post_setup(self): + ldapi_db = Ldb(self.ldap_uri, credentials=self.credentials) + + # configure in-directory access control on Fedora DS via the aci + # attribute (over a direct ldapi:// socket) + aci = """(targetattr = "*") (version 3.0;acl "full access to all by samba-admin";allow (all)(userdn = "ldap:///CN=samba-admin,%s");)""" % self.sambadn + + m = ldb.Message() + m["aci"] = ldb.MessageElement([aci], ldb.FLAG_MOD_REPLACE, "aci") + + for dnstring in (self.names.domaindn, self.names.configdn, + self.names.schemadn): + m.dn = ldb.Dn(ldapi_db, dnstring) + ldapi_db.modify(m) + return LDAPBackendResult(self.credentials, self.slapd_command_escaped, + self.ldapdir) diff --git a/python/samba/provision/common.py b/python/samba/provision/common.py new file mode 100644 index 00000000000..f96704bccee --- /dev/null +++ b/python/samba/provision/common.py @@ -0,0 +1,82 @@ + +# Unix SMB/CIFS implementation. +# utility functions for provisioning a Samba4 server + +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2010 +# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2008-2009 +# Copyright (C) Oliver Liebel <oliver@itc.li> 2008-2009 +# +# Based on the original in EJS: +# Copyright (C) Andrew Tridgell <tridge@samba.org> 2005 +# +# 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/>. +# + +"""Functions for setting up a Samba configuration.""" + +__docformat__ = "restructuredText" + +import os +from samba import read_and_sub_file +from samba.param import setup_dir + + +def setup_path(file): + """Return an absolute path to the provision tempate file specified by file""" + return os.path.join(setup_dir(), file) + + +def setup_add_ldif(ldb, ldif_path, subst_vars=None,controls=["relax:0"]): + """Setup a ldb in the private dir. + + :param ldb: LDB file to import data into + :param ldif_path: Path of the LDIF file to load + :param subst_vars: Optional variables to subsitute in LDIF. + :param nocontrols: Optional list of controls, can be None for no controls + """ + assert isinstance(ldif_path, str) + data = read_and_sub_file(ldif_path, subst_vars) + ldb.add_ldif(data, controls) + + +def setup_modify_ldif(ldb, ldif_path, subst_vars=None,controls=["relax:0"]): + """Modify a ldb in the private dir. + + :param ldb: LDB object. + :param ldif_path: LDIF file path. + :param subst_vars: Optional dictionary with substitution variables. + """ + data = read_and_sub_file(ldif_path, subst_vars) + ldb.modify_ldif(data, controls) + + +def setup_ldb(ldb, ldif_path, subst_vars): + """Import a LDIF a file into a LDB handle, optionally substituting + variables. + + :note: Either all LDIF data will be added or none (using transactions). + + :param ldb: LDB file to import into. + :param ldif_path: Path to the LDIF file. + :param subst_vars: Dictionary with substitution variables. + """ + assert ldb is not None + ldb.transaction_start() + try: + setup_add_ldif(ldb, ldif_path, subst_vars) + except: + ldb.transaction_cancel() + raise + else: + ldb.transaction_commit() diff --git a/python/samba/provision/descriptor.py b/python/samba/provision/descriptor.py new file mode 100644 index 00000000000..32e91ed2b57 --- /dev/null +++ b/python/samba/provision/descriptor.py @@ -0,0 +1,359 @@ + +# Unix SMB/CIFS implementation. +# backend code for provisioning a Samba4 server + +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2010 +# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2008-2009 +# Copyright (C) Oliver Liebel <oliver@itc.li> 2008-2009 +# Copyright (C) Amitay Isaacs <amitay@samba.org> 2011 +# +# Based on the original in EJS: +# Copyright (C) Andrew Tridgell <tridge@samba.org> 2005 +# +# 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/>. +# + +"""Functions for setting up a Samba configuration (security descriptors).""" + +from samba.dcerpc import security +from samba.ndr import ndr_pack + +# Descriptors of naming contexts and other important objects + +def sddl2binary(sddl_in, domain_sid, name_map): + sddl = "%s" % sddl_in + + for [name, sid] in name_map.items(): + sddl = sddl.replace(name, sid) + + sec = security.descriptor.from_sddl(sddl, domain_sid) + return ndr_pack(sec) + +def get_empty_descriptor(domain_sid, name_map={}): + sddl= "" + return sddl2binary(sddl, domain_sid, name_map) + +# "get_schema_descriptor" is located in "schema.py" + +def get_config_descriptor(domain_sid, name_map={}): + sddl = "O:EAG:EAD:(OA;;CR;1131f6aa-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \ + "(OA;;CR;1131f6ab-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \ + "(OA;;CR;1131f6ac-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \ + "(OA;;CR;1131f6aa-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(OA;;CR;1131f6ab-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(OA;;CR;1131f6ac-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(A;;RPLCLORC;;;AU)(A;CI;RPWPCRCCDCLCLORCWOWDSDDTSW;;;EA)" \ + "(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)(A;CIIO;RPWPCRCCLCLORCWOWDSDSW;;;DA)" \ + "(OA;;CR;1131f6ad-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \ + "(OA;;CR;89e95b76-444d-4c62-991a-0facbeda640c;;ED)" \ + "(OA;;CR;1131f6ad-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(OA;;CR;89e95b76-444d-4c62-991a-0facbeda640c;;BA)" \ + "(OA;;CR;1131f6aa-9c07-11d1-f79f-00c04fc2dcd2;;ER)" \ + "S:(AU;SA;WPWOWD;;;WD)(AU;SA;CR;;;BA)(AU;SA;CR;;;DU)" \ + "(OU;SA;CR;45ec5156-db7e-47bb-b53f-dbeb2d03c40f;;WD)" + return sddl2binary(sddl, domain_sid, name_map) + +def get_config_partitions_descriptor(domain_sid, name_map={}): + sddl = "D:" \ + "(A;;LCLORC;;;AU)" \ + "(OA;;RP;e48d0154-bcf8-11d1-8702-00c04fb96050;;AU)" \ + "(OA;;RP;d31a8757-2447-4545-8081-3bb610cacbf2;;AU)" \ + "(OA;;RP;66171887-8f3c-11d0-afda-00c04fd930c9;;AU)" \ + "(OA;;RP;032160bf-9824-11d1-aec0-0000f80367c1;;AU)" \ + "(OA;;RP;789ee1eb-8c8e-4e4c-8cec-79b31b7617b5;;AU)" \ + "(OA;;RP;5706aeaf-b940-4fb2-bcfc-5268683ad9fe;;AU)" \ + "(A;;RPWPCRCCLCLORCWOWDSW;;;EA)" \ + "(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)" \ + "(A;;CC;;;ED)" \ + "(OA;CIIO;WP;3df793df-9858-4417-a701-735a1ecebf74;bf967a8d-0de6-11d0-a285-00aa003049e2;BA)" \ + "S:" \ + "(AU;CISA;WPCRCCDCWOWDSDDT;;;WD)" + return sddl2binary(sddl, domain_sid, name_map) + +def get_config_sites_descriptor(domain_sid, name_map={}): + sddl = "D:" \ + "(A;;RPLCLORC;;;AU)" \ + "(OA;CIIO;SW;d31a8757-2447-4545-8081-3bb610cacbf2;f0f8ffab-1191-11d0-a060-00aa006c33ed;ER)" \ + "(A;;RPWPCRCCLCLORCWOWDSW;;;EA)" \ + "(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)" \ + "S:" \ + "(AU;CISA;CCDCSDDT;;;WD)" \ + "(OU;CIIOSA;CR;;f0f8ffab-1191-11d0-a060-00aa006c33ed;WD)" \ + "(OU;CIIOSA;WP;f30e3bbe-9ff0-11d1-b603-0000f80367c1;bf967ab3-0de6-11d0-a285-00aa003049e2;WD)" \ + "(OU;CIIOSA;WP;f30e3bbf-9ff0-11d1-b603-0000f80367c1;bf967ab3-0de6-11d0-a285-00aa003049e2;WD)" \ + "(OU;CIIOSA;WP;3e10944c-c354-11d0-aff8-0000f80367c1;b7b13124-b82e-11d0-afee-0000f80367c1;WD)" + return sddl2binary(sddl, domain_sid, name_map) + +def get_config_ntds_quotas_descriptor(domain_sid, name_map={}): + sddl = "D:" \ + "(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;EA)" \ + "(A;;RPLCLORC;;;BA)" \ + "(OA;;CR;4ecc03fe-ffc0-4947-b630-eb672a8a9dbc;;WD)" + return sddl2binary(sddl, domain_sid, name_map) + +def get_config_delete_protected1_descriptor(domain_sid, name_map={}): + sddl = "D:AI" \ + "(A;;RPLCLORC;;;AU)" \ + "(A;;RPWPCRCCLCLORCWOWDSW;;;EA)" \ + "(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)" + return sddl2binary(sddl, domain_sid, name_map) + +def get_config_delete_protected1wd_descriptor(domain_sid, name_map={}): + sddl = "D:AI" \ + "(A;;RPLCLORC;;;WD)" \ + "(A;;RPWPCRCCLCLORCWOWDSW;;;EA)" \ + "(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)" + return sddl2binary(sddl, domain_sid, name_map) + +def get_config_delete_protected2_descriptor(domain_sid, name_map={}): + sddl = "D:AI" \ + "(A;;RPLCLORC;;;AU)" \ + "(A;;RPWPCRCCDCLCLORCWOWDSW;;;EA)" \ + "(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)" + return sddl2binary(sddl, domain_sid, name_map) + +def get_domain_descriptor(domain_sid, name_map={}): + sddl= "O:BAG:BAD:AI(OA;CIIO;RP;4c164200-20c0-11d0-a768-00aa006e0529;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)" \ + "(OA;CIIO;RP;4c164200-20c0-11d0-a768-00aa006e0529;bf967aba-0de6-11d0-a285-00aa003049e2;RU)" \ + "(OA;CIIO;RP;5f202010-79a5-11d0-9020-00c04fc2d4cf;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)" \ + "(OA;CIIO;RP;5f202010-79a5-11d0-9020-00c04fc2d4cf;bf967aba-0de6-11d0-a285-00aa003049e2;RU)" \ + "(OA;CIIO;RP;bc0ac240-79a9-11d0-9020-00c04fc2d4cf;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)" \ + "(OA;CIIO;RP;bc0ac240-79a9-11d0-9020-00c04fc2d4cf;bf967aba-0de6-11d0-a285-00aa003049e2;RU)" \ + "(OA;CIIO;RP;59ba2f42-79a2-11d0-9020-00c04fc2d3cf;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)" \ + "(OA;CIIO;RP;59ba2f42-79a2-11d0-9020-00c04fc2d3cf;bf967aba-0de6-11d0-a285-00aa003049e2;RU)" \ + "(OA;CIIO;RP;037088f8-0ae1-11d2-b422-00a0c968f939;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)" \ + "(OA;CIIO;RP;037088f8-0ae1-11d2-b422-00a0c968f939;bf967aba-0de6-11d0-a285-00aa003049e2;RU)" \ + "(OA;;CR;1131f6aa-9c07-11d1-f79f-00c04fc2dcd2;;ER)" \ + "(OA;;CR;1131f6ad-9c07-11d1-f79f-00c04fc2dcd2;;DD)" \ + "(OA;CIIO;RP;b7c69e6d-2cc7-11d2-854e-00a0c983f608;bf967a86-0de6-11d0-a285-00aa003049e2;ED)" \ + "(OA;CIIO;RP;b7c69e6d-2cc7-11d2-854e-00a0c983f608;bf967a9c-0de6-11d0-a285-00aa003049e2;ED)" \ + "(OA;CIIO;RP;b7c69e6d-2cc7-11d2-854e-00a0c983f608;bf967aba-0de6-11d0-a285-00aa003049e2;ED)" \ + "(OA;;CR;89e95b76-444d-4c62-991a-0facbeda640c;;BA)" \ + "(OA;;CR;1131f6aa-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(OA;;CR;1131f6ab-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(OA;;CR;1131f6ac-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(OA;;CR;1131f6ad-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(OA;;CR;1131f6ae-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(OA;;CR;e2a36dc9-ae17-47c3-b58b-be34c55ba633;;IF)" \ + "(OA;;RP;c7407360-20bf-11d0-a768-00aa006e0529;;RU)" \ + "(OA;;RP;b8119fd0-04f6-4762-ab7a-4986c76b3f9a;;RU)" \ + "(OA;CIIO;RPLCLORC;;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)" \ + "(OA;CIIO;RPLCLORC;;bf967a9c-0de6-11d0-a285-00aa003049e2;RU)" \ + "(OA;CIIO;RPLCLORC;;bf967aba-0de6-11d0-a285-00aa003049e2;RU)" \ + "(OA;;CR;05c74c5e-4deb-43b4-bd9f-86664c2a7fd5;;AU)" \ + "(OA;;CR;89e95b76-444d-4c62-991a-0facbeda640c;;ED)" \ + "(OA;;CR;ccc2dc7d-a6ad-4a7a-8846-c04e3cc53501;;AU)" \ + "(OA;;CR;280f369c-67c7-438e-ae98-1d46f3c6f541;;AU)" \ + "(OA;;CR;1131f6aa-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \ + "(OA;;CR;1131f6ab-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \ + "(OA;;CR;1131f6ac-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \ + "(OA;;CR;1131f6ae-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \ + "(OA;;RP;b8119fd0-04f6-4762-ab7a-4986c76b3f9a;;AU)" \ + "(OA;CIIO;RPWPCR;91e647de-d96f-4b70-9557-d63ff4f3ccd8;;PS)" \ + "(A;;RPWPCRCCLCLORCWOWDSW;;;DA)" \ + "(A;CI;RPWPCRCCDCLCLORCWOWDSDDTSW;;;EA)" \ + "(A;;RPRC;;;RU)" \ + "(A;CI;LC;;;RU)" \ + "(A;CI;RPWPCRCCLCLORCWOWDSDSW;;;BA)" \ + "(A;;RP;;;WD)" \ + "(A;;RPLCLORC;;;ED)" \ + "(A;;RPLCLORC;;;AU)" \ + "(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)" \ + "S:AI(OU;CISA;WP;f30e3bbe-9ff0-11d1-b603-0000f80367c1;bf967aa5-0de6-11d0-a285-00aa003049e2;WD)" \ + "(OU;CISA;WP;f30e3bbf-9ff0-11d1-b603-0000f80367c1;bf967aa5-0de6-11d0-a285-00aa003049e2;WD)" \ + "(AU;SA;CR;;;DU)(AU;SA;CR;;;BA)(AU;SA;WPWOWD;;;WD)" + return sddl2binary(sddl, domain_sid, name_map) + +def get_domain_infrastructure_descriptor(domain_sid, name_map={}): + sddl = "D:" \ + "(A;;RPLCLORC;;;AU)" \ + "(A;;RPWPCRCCLCLORCWOWDSW;;;DA)" \ + "(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)" \ + "S:" \ + "(AU;SA;WPCR;;;WD)" + return sddl2binary(sddl, domain_sid, name_map) + +def get_domain_builtin_descriptor(domain_sid, name_map={}): + sddl = "D:" \ + "(OA;CIIO;RP;4c164200-20c0-11d0-a768-00aa006e0529;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)" \ + "(OA;CIIO;RP;4c164200-20c0-11d0-a768-00aa006e0529;bf967aba-0de6-11d0-a285-00aa003049e2;RU)" \ + "(OA;CIIO;RP;5f202010-79a5-11d0-9020-00c04fc2d4cf;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)" \ + "(OA;CIIO;RP;5f202010-79a5-11d0-9020-00c04fc2d4cf;bf967aba-0de6-11d0-a285-00aa003049e2;RU)" \ + "(OA;CIIO;RP;bc0ac240-79a9-11d0-9020-00c04fc2d4cf;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)" \ + "(OA;CIIO;RP;bc0ac240-79a9-11d0-9020-00c04fc2d4cf;bf967aba-0de6-11d0-a285-00aa003049e2;RU)" \ + "(OA;CIIO;RP;59ba2f42-79a2-11d0-9020-00c04fc2d3cf;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)" \ + "(OA;CIIO;RP;59ba2f42-79a2-11d0-9020-00c04fc2d3cf;bf967aba-0de6-11d0-a285-00aa003049e2;RU)" \ + "(OA;CIIO;RP;037088f8-0ae1-11d2-b422-00a0c968f939;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)" \ + "(OA;CIIO;RP;037088f8-0ae1-11d2-b422-00a0c968f939;bf967aba-0de6-11d0-a285-00aa003049e2;RU)" \ + "(OA;;CR;1131f6aa-9c07-11d1-f79f-00c04fc2dcd2;;ER)" \ + "(OA;;CR;1131f6ad-9c07-11d1-f79f-00c04fc2dcd2;;DD)" \ + "(OA;CIIO;RP;b7c69e6d-2cc7-11d2-854e-00a0c983f608;bf967a86-0de6-11d0-a285-00aa003049e2;ED)" \ + "(OA;CIIO;RP;b7c69e6d-2cc7-11d2-854e-00a0c983f608;bf967a9c-0de6-11d0-a285-00aa003049e2;ED)" \ + "(OA;CIIO;RP;b7c69e6d-2cc7-11d2-854e-00a0c983f608;bf967aba-0de6-11d0-a285-00aa003049e2;ED)" \ + "(OA;;CR;89e95b76-444d-4c62-991a-0facbeda640c;;BA)" \ + "(OA;;CR;1131f6aa-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(OA;;CR;1131f6ab-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(OA;;CR;1131f6ac-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(OA;;CR;1131f6ad-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(OA;;CR;1131f6ae-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(OA;;CR;e2a36dc9-ae17-47c3-b58b-be34c55ba633;;IF)" \ + "(OA;;RP;c7407360-20bf-11d0-a768-00aa006e0529;;RU)" \ + "(OA;;RP;b8119fd0-04f6-4762-ab7a-4986c76b3f9a;;RU)" \ + "(OA;CIIO;RPLCLORC;;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)" \ + "(OA;CIIO;RPLCLORC;;bf967a9c-0de6-11d0-a285-00aa003049e2;RU)" \ + "(OA;CIIO;RPLCLORC;;bf967aba-0de6-11d0-a285-00aa003049e2;RU)" \ + "(OA;;CR;05c74c5e-4deb-43b4-bd9f-86664c2a7fd5;;AU)" \ + "(OA;;CR;89e95b76-444d-4c62-991a-0facbeda640c;;ED)" \ + "(OA;;CR;ccc2dc7d-a6ad-4a7a-8846-c04e3cc53501;;AU)" \ + "(OA;;CR;280f369c-67c7-438e-ae98-1d46f3c6f541;;AU)" \ + "(OA;;CR;1131f6aa-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \ + "(OA;;CR;1131f6ab-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \ + "(OA;;CR;1131f6ac-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \ + "(OA;;CR;1131f6ae-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \ + "(OA;;RP;b8119fd0-04f6-4762-ab7a-4986c76b3f9a;;AU)" \ + "(OA;CIIO;RPWPCR;91e647de-d96f-4b70-9557-d63ff4f3ccd8;;PS)" \ + "(A;;RPWPCRCCLCLORCWOWDSW;;;DA)" \ + "(A;CI;RPWPCRCCDCLCLORCWOWDSDDTSW;;;EA)" \ + "(A;;RPRC;;;RU)" \ + "(A;CI;LC;;;RU)" \ + "(A;CI;RPWPCRCCLCLORCWOWDSDSW;;;BA)" \ + "(A;;RP;;;WD)" \ + "(A;;RPLCLORC;;;ED)" \ + "(A;;RPLCLORC;;;AU)" \ + "(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)" \ + "S:" \ + "(OU;CISA;WP;f30e3bbe-9ff0-11d1-b603-0000f80367c1;bf967aa5-0de6-11d0-a285-00aa003049e2;WD)" \ + "(OU;CISA;WP;f30e3bbf-9ff0-11d1-b603-0000f80367c1;bf967aa5-0de6-11d0-a285-00aa003049e2;WD)" \ + "(AU;SA;CR;;;DU)" \ + "(AU;SA;CR;;;BA)" \ + "(AU;SA;WPWOWD;;;WD)" + return sddl2binary(sddl, domain_sid, name_map) + +def get_domain_computers_descriptor(domain_sid, name_map={}): + sddl = "D:" \ + "(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)" \ + "(A;;RPWPCRCCDCLCLORCWOWDSW;;;DA)" \ + "(OA;;CCDC;bf967a86-0de6-11d0-a285-00aa003049e2;;AO)" \ + "(OA;;CCDC;bf967aba-0de6-11d0-a285-00aa003049e2;;AO)" \ + "(OA;;CCDC;bf967a9c-0de6-11d0-a285-00aa003049e2;;AO)" \ + "(OA;;CCDC;bf967aa8-0de6-11d0-a285-00aa003049e2;;PO)" \ + "(A;;RPLCLORC;;;AU)" \ + "(OA;;CCDC;4828cc14-1437-45bc-9b07-ad6f015e5f28;;AO)" \ + "S:" + return sddl2binary(sddl, domain_sid, name_map) + +def get_domain_users_descriptor(domain_sid, name_map={}): + sddl = "D:" \ + "(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)" \ + "(A;;RPWPCRCCDCLCLORCWOWDSW;;;DA)" \ + "(OA;;CCDC;bf967aba-0de6-11d0-a285-00aa003049e2;;AO)" \ + "(OA;;CCDC;bf967a9c-0de6-11d0-a285-00aa003049e2;;AO)" \ + "(OA;;CCDC;bf967aa8-0de6-11d0-a285-00aa003049e2;;PO)" \ + "(A;;RPLCLORC;;;AU)" \ + "(OA;;CCDC;4828cc14-1437-45bc-9b07-ad6f015e5f28;;AO)" \ + "S:" + return sddl2binary(sddl, domain_sid, name_map) + +def get_domain_controllers_descriptor(domain_sid, name_map={}): + sddl = "D:" \ + "(A;;RPLCLORC;;;AU)" \ + "(A;;RPWPCRCCLCLORCWOWDSW;;;DA)" \ + "(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)" \ + "(A;;RPLCLORC;;;ED)" \ + "S:" \ + "(AU;SA;CCDCWOWDSDDT;;;WD)" \ + "(AU;CISA;WP;;;WD)" + return sddl2binary(sddl, domain_sid, name_map) + +def get_domain_delete_protected1_descriptor(domain_sid, name_map={}): + sddl = "D:AI" \ + "(A;;RPLCLORC;;;AU)" \ + "(A;;RPWPCRCCLCLORCWOWDSW;;;DA)" \ + "(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)" + return sddl2binary(sddl, domain_sid, name_map) + +def get_domain_delete_protected2_descriptor(domain_sid, name_map={}): + sddl = "D:AI" \ + "(A;;RPLCLORC;;;AU)" \ + "(A;;RPWPCRCCDCLCLORCWOWDSW;;;DA)" \ + "(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)" + return sddl2binary(sddl, domain_sid, name_map) + +def get_dns_partition_descriptor(domain_sid, name_map={}): + sddl = "O:SYG:BAD:AI" \ + "(OA;CIIO;RP;4c164200-20c0-11d0-a768-00aa006e0529;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)" \ + "(OA;CIIO;RP;4c164200-20c0-11d0-a768-00aa006e0529;bf967aba-0de6-11d0-a285-00aa003049e2;RU)" \ + "(OA;CIIO;RP;5f202010-79a5-11d0-9020-00c04fc2d4cf;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)" \ + "(OA;CIIO;RP;5f202010-79a5-11d0-9020-00c04fc2d4cf;bf967aba-0de6-11d0-a285-00aa003049e2;RU)" \ + "(OA;CIIO;RP;bc0ac240-79a9-11d0-9020-00c04fc2d4cf;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)" \ + "(OA;CIIO;RP;bc0ac240-79a9-11d0-9020-00c04fc2d4cf;bf967aba-0de6-11d0-a285-00aa003049e2;RU)" \ + "(OA;CIIO;RP;59ba2f42-79a2-11d0-9020-00c04fc2d3cf;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)" \ + "(OA;CIIO;RP;59ba2f42-79a2-11d0-9020-00c04fc2d3cf;bf967aba-0de6-11d0-a285-00aa003049e2;RU)" \ + "(OA;CIIO;RP;037088f8-0ae1-11d2-b422-00a0c968f939;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)" \ + "(OA;CIIO;RP;037088f8-0ae1-11d2-b422-00a0c968f939;bf967aba-0de6-11d0-a285-00aa003049e2;RU)" \ + "(OA;;CR;1131f6aa-9c07-11d1-f79f-00c04fc2dcd2;;ER)" \ + "(OA;CIIO;RP;b7c69e6d-2cc7-11d2-854e-00a0c983f608;bf967a86-0de6-11d0-a285-00aa003049e2;ED)" \ + "(OA;CIIO;RP;b7c69e6d-2cc7-11d2-854e-00a0c983f608;bf967a9c-0de6-11d0-a285-00aa003049e2;ED)" \ + "(OA;CIIO;RP;b7c69e6d-2cc7-11d2-854e-00a0c983f608;bf967aba-0de6-11d0-a285-00aa003049e2;ED)" \ + "(OA;;CR;89e95b76-444d-4c62-991a-0facbeda640c;;BA)" \ + "(OA;;CR;1131f6aa-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(OA;;CR;1131f6ab-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(OA;;CR;1131f6ac-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(OA;;CR;1131f6ad-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(OA;;CR;1131f6ae-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(OA;;CR;e2a36dc9-ae17-47c3-b58b-be34c55ba633;;IF)" \ + "(OA;;RP;c7407360-20bf-11d0-a768-00aa006e0529;;RU)" \ + "(OA;;RP;b8119fd0-04f6-4762-ab7a-4986c76b3f9a;;RU)" \ + "(OA;CIIO;RPLCLORC;;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)" \ + "(OA;CIIO;RPLCLORC;;bf967a9c-0de6-11d0-a285-00aa003049e2;RU)" \ + "(OA;CIIO;RPLCLORC;;bf967aba-0de6-11d0-a285-00aa003049e2;RU)" \ + "(OA;;CR;05c74c5e-4deb-43b4-bd9f-86664c2a7fd5;;AU)" \ + "(OA;;CR;89e95b76-444d-4c62-991a-0facbeda640c;;ED)" \ + "(OA;;CR;ccc2dc7d-a6ad-4a7a-8846-c04e3cc53501;;AU)" \ + "(OA;;CR;280f369c-67c7-438e-ae98-1d46f3c6f541;;AU)" \ + "(OA;;CR;1131f6aa-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \ + "(OA;;CR;1131f6ab-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \ + "(OA;;CR;1131f6ac-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \ + "(OA;;CR;1131f6ad-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \ + "(OA;;CR;1131f6ae-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \ + "(OA;;RP;b8119fd0-04f6-4762-ab7a-4986c76b3f9a;;AU)" \ + "(OA;CIIO;RPWPCR;91e647de-d96f-4b70-9557-d63ff4f3ccd8;;PS)" \ + "(A;;RPWPCRCCLCLORCWOWDSW;;;DA)" \ + "(A;CI;RPWPCRCCDCLCLORCWOWDSDDTSW;;;EA)" \ + "(A;;RPRC;;;RU)" \ + "(A;CI;LC;;;RU)" \ + "(A;CI;RPWPCRCCLCLORCWOWDSDSW;;;BA)" \ + "(A;;RP;;;WD)" \ + "(A;;RPLCLORC;;;ED)" \ + "(A;;RPLCLORC;;;AU)" \ + "(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)" \ + "S:AI" \ + "(OU;CISA;WP;f30e3bbe-9ff0-11d1-b603-0000f80367c1;bf967aa5-0de6-11d0-a285-00aa003049e2;WD)" \ + "(OU;CISA;WP;f30e3bbf-9ff0-11d1-b603-0000f80367c1;bf967aa5-0de6-11d0-a285-00aa003049e2;WD)" \ + "(AU;SA;CR;;;DU)(AU;SA;CR;;;BA)(AU;SA;WPWOWD;;;WD)" + return sddl2binary(sddl, domain_sid, name_map) + +def get_dns_forest_microsoft_dns_descriptor(domain_sid, name_map={}): + sddl = "O:SYG:SYD:AI" \ + "(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)" \ + "(A;CI;RPWPCRCCDCLCRCWOWDSDDTSW;;;ED)" + return sddl2binary(sddl, domain_sid, name_map) + +def get_dns_domain_microsoft_dns_descriptor(domain_sid, name_map={}): + sddl = "O:SYG:SYD:AI" \ + "(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;DA)" \ + "(A;CI;RPWPCRCCDCLCRCWOWDSDDTSW;;;DnsAdmins)" \ + "(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)" \ + "(A;CI;RPWPCRCCDCLCRCWOWDSDDTSW;;;ED)" + return sddl2binary(sddl, domain_sid, name_map) diff --git a/python/samba/provision/sambadns.py b/python/samba/provision/sambadns.py new file mode 100644 index 00000000000..4522683fe8b --- /dev/null +++ b/python/samba/provision/sambadns.py @@ -0,0 +1,1135 @@ +# Unix SMB/CIFS implementation. +# backend code for provisioning DNS for a Samba4 server +# +# Copyright (C) Kai Blin <kai@samba.org> 2011 +# Copyright (C) Amitay Isaacs <amitay@gmail.com> 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/>. +# + +"""DNS-related provisioning""" + +import os +import uuid +import shutil +import time +import ldb +from base64 import b64encode +import samba +from samba.tdb_util import tdb_copy +from samba.ndr import ndr_pack, ndr_unpack +from samba import setup_file +from samba.dcerpc import dnsp, misc, security +from samba.dsdb import ( + DS_DOMAIN_FUNCTION_2000, + DS_DOMAIN_FUNCTION_2003, + DS_DOMAIN_FUNCTION_2008_R2 + ) +from samba.provision.descriptor import ( + get_domain_descriptor, + get_domain_delete_protected1_descriptor, + get_domain_delete_protected2_descriptor, + get_dns_partition_descriptor, + get_dns_forest_microsoft_dns_descriptor, + get_dns_domain_microsoft_dns_descriptor + ) +from samba.provision.common import ( + setup_path, + setup_add_ldif, + setup_modify_ldif, + setup_ldb + ) + + +def get_domainguid(samdb, domaindn): + res = samdb.search(base=domaindn, scope=ldb.SCOPE_BASE, attrs=["objectGUID"]) + domainguid = str(ndr_unpack(misc.GUID, res[0]["objectGUID"][0])) + return domainguid + + +def get_dnsadmins_sid(samdb, domaindn): + res = samdb.search(base="CN=DnsAdmins,CN=Users,%s" % domaindn, scope=ldb.SCOPE_BASE, + attrs=["objectSid"]) + dnsadmins_sid = ndr_unpack(security.dom_sid, res[0]["objectSid"][0]) + return dnsadmins_sid + + +class ARecord(dnsp.DnssrvRpcRecord): + + def __init__(self, ip_addr, serial=1, ttl=900, rank=dnsp.DNS_RANK_ZONE): + super(ARecord, self).__init__() + self.wType = dnsp.DNS_TYPE_A + self.rank = rank + self.dwSerial = serial + self.dwTtlSeconds = ttl + self.data = ip_addr + + +class AAAARecord(dnsp.DnssrvRpcRecord): + + def __init__(self, ip6_addr, serial=1, ttl=900, rank=dnsp.DNS_RANK_ZONE): + super(AAAARecord, self).__init__() + self.wType = dnsp.DNS_TYPE_AAAA + self.rank = rank + self.dwSerial = serial + self.dwTtlSeconds = ttl + self.data = ip6_addr + + +class CNameRecord(dnsp.DnssrvRpcRecord): + + def __init__(self, cname, serial=1, ttl=900, rank=dnsp.DNS_RANK_ZONE): + super(CNameRecord, self).__init__() + self.wType = dnsp.DNS_TYPE_CNAME + self.rank = rank + self.dwSerial = serial + self.dwTtlSeconds = ttl + self.data = cname + + +class NSRecord(dnsp.DnssrvRpcRecord): + + def __init__(self, dns_server, serial=1, ttl=900, rank=dnsp.DNS_RANK_ZONE): + super(NSRecord, self).__init__() + self.wType = dnsp.DNS_TYPE_NS + self.rank = rank + self.dwSerial = serial + self.dwTtlSeconds = ttl + self.data = dns_server + + +class SOARecord(dnsp.DnssrvRpcRecord): + + def __init__(self, mname, rname, serial=1, refresh=900, retry=600, + expire=86400, minimum=3600, ttl=3600, rank=dnsp.DNS_RANK_ZONE): + super(SOARecord, self).__init__() + self.wType = dnsp.DNS_TYPE_SOA + self.rank = rank + self.dwSerial = serial + self.dwTtlSeconds = ttl + soa = dnsp.soa() + soa.serial = serial + soa.refresh = refresh + soa.retry = retry + soa.expire = expire + soa.mname = mname + soa.rname = rname + self.data = soa + + +class SRVRecord(dnsp.DnssrvRpcRecord): + + def __init__(self, target, port, priority=0, weight=100, serial=1, ttl=900, + rank=dnsp.DNS_RANK_ZONE): + super(SRVRecord, self).__init__() + self.wType = dnsp.DNS_TYPE_SRV + self.rank = rank + self.dwSerial = serial + self.dwTtlSeconds = ttl + srv = dnsp.srv() + srv.nameTarget = target + srv.wPort = port + srv.wPriority = priority + srv.wWeight = weight + self.data = srv + + +class TXTRecord(dnsp.DnssrvRpcRecord): + + def __init__(self, slist, serial=1, ttl=900, rank=dnsp.DNS_RANK_ZONE): + super(TXTRecord, self).__init__() + self.wType = dnsp.DNS_TYPE_TXT + self.rank = rank + self.dwSerial = serial + self.dwTtlSeconds = ttl + stringlist = dnsp.string_list() + stringlist.count = len(slist) + stringlist.str = slist + self.data = stringlist + + +class TypeProperty(dnsp.DnsProperty): + + def __init__(self, zone_type=dnsp.DNS_ZONE_TYPE_PRIMARY): + super(TypeProperty, self).__init__() + self.wDataLength = 1 + self.version = 1 + self.id = dnsp.DSPROPERTY_ZONE_TYPE + self.data = zone_type + + +class AllowUpdateProperty(dnsp.DnsProperty): + + def __init__(self, allow_update=dnsp.DNS_ZONE_UPDATE_SECURE): + super(AllowUpdateProperty, self).__init__() + self.wDataLength = 1 + self.version = 1 + self.id = dnsp.DSPROPERTY_ZONE_ALLOW_UPDATE + self.data = allow_update + + +class SecureTimeProperty(dnsp.DnsProperty): + + def __init__(self, secure_time=0): + super(SecureTimeProperty, self).__init__() + self.wDataLength = 1 + self.version = 1 + self.id = dnsp.DSPROPERTY_ZONE_SECURE_TIME + self.data = secure_time + + +class NorefreshIntervalProperty(dnsp.DnsProperty): + + def __init__(self, norefresh_interval=0): + super(NorefreshIntervalProperty, self).__init__() + self.wDataLength = 1 + self.version = 1 + self.id = dnsp.DSPROPERTY_ZONE_NOREFRESH_INTERVAL + self.data = norefresh_interval + + +class RefreshIntervalProperty(dnsp.DnsProperty): + + def __init__(self, refresh_interval=0): + super(RefreshIntervalProperty, self).__init__() + self.wDataLength = 1 + self.version = 1 + self.id = dnsp.DSPROPERTY_ZONE_REFRESH_INTERVAL + self.data = refresh_interval + + +class AgingStateProperty(dnsp.DnsProperty): + + def __init__(self, aging_enabled=0): + super(AgingStateProperty, self).__init__() + self.wDataLength = 1 + self.version = 1 + self.id = dnsp.DSPROPERTY_ZONE_AGING_STATE + self.data = aging_enabled + + +class AgingEnabledTimeProperty(dnsp.DnsProperty): + + def __init__(self, next_cycle_hours=0): + super(AgingEnabledTimeProperty, self).__init__() + self.wDataLength = 1 + self.version = 1; + self.id = dnsp.DSPROPERTY_ZONE_AGING_ENABLED_TIME + self.data = next_cycle_hours + + +def setup_dns_partitions(samdb, domainsid, domaindn, forestdn, configdn, + serverdn): + domainzone_dn = "DC=DomainDnsZones,%s" % domaindn + forestzone_dn = "DC=ForestDnsZones,%s" % forestdn + descriptor = get_dns_partition_descriptor(domainsid) + setup_add_ldif(samdb, setup_path("provision_dnszones_partitions.ldif"), { + "DOMAINZONE_DN": domainzone_dn, + "FORESTZONE_DN": forestzone_dn, + "SECDESC" : b64encode(descriptor) + }) + + domainzone_guid = get_domainguid(samdb, domainzone_dn) + forestzone_guid = get_domainguid(samdb, forestzone_dn) + + domainzone_guid = str(uuid.uuid4()) + forestzone_guid = str(uuid.uuid4()) + + domainzone_dns = ldb.Dn(samdb, domainzone_dn).canonical_ex_str().strip() + forestzone_dns = ldb.Dn(samdb, forestzone_dn).canonical_ex_str().strip() + + protected1_desc = get_domain_delete_protected1_descriptor(domainsid) + protected2_desc = get_domain_delete_protected2_descriptor(domainsid) + setup_add_ldif(samdb, setup_path("provision_dnszones_add.ldif"), { + "DOMAINZONE_DN": domainzone_dn, + "FORESTZONE_DN": forestzone_dn, + "DOMAINZONE_GUID": domainzone_guid, + "FORESTZONE_GUID": forestzone_guid, + "DOMAINZONE_DNS": domainzone_dns, + "FORESTZONE_DNS": forestzone_dns, + "CONFIGDN": configdn, + "SERVERDN": serverdn, + "LOSTANDFOUND_DESCRIPTOR": b64encode(protected2_desc), + "INFRASTRUCTURE_DESCRIPTOR": b64encode(protected1_desc), + }) + + setup_modify_ldif(samdb, setup_path("provision_dnszones_modify.ldif"), { + "CONFIGDN": configdn, + "SERVERDN": serverdn, + "DOMAINZONE_DN": domainzone_dn, + "FORESTZONE_DN": forestzone_dn, + }) + + +def add_dns_accounts(samdb, domaindn): + setup_add_ldif(samdb, setup_path("provision_dns_accounts_add.ldif"), { + "DOMAINDN": domaindn, + }) + + +def add_dns_container(samdb, domaindn, prefix, domain_sid, dnsadmins_sid, forest=False): + name_map = {'DnsAdmins': str(dnsadmins_sid)} + if forest is True: + sd_val = get_dns_forest_microsoft_dns_descriptor(domain_sid, + name_map=name_map) + else: + sd_val = get_dns_domain_microsoft_dns_descriptor(domain_sid, + name_map=name_map) + # CN=MicrosoftDNS,<PREFIX>,<DOMAINDN> + msg = ldb.Message(ldb.Dn(samdb, "CN=MicrosoftDNS,%s,%s" % (prefix, domaindn))) + msg["objectClass"] = ["top", "container"] + msg["nTSecurityDescriptor"] = ldb.MessageElement(sd_val, ldb.FLAG_MOD_ADD, + "nTSecurityDescriptor") + samdb.add(msg) + + +def add_rootservers(samdb, domaindn, prefix): + rootservers = {} + rootservers["a.root-servers.net"] = "198.41.0.4" + rootservers["b.root-servers.net"] = "192.228.79.201" + rootservers["c.root-servers.net"] = "192.33.4.12" + rootservers["d.root-servers.net"] = "128.8.10.90" + rootservers["e.root-servers.net"] = "192.203.230.10" + rootservers["f.root-servers.net"] = "192.5.5.241" + rootservers["g.root-servers.net"] = "192.112.36.4" + rootservers["h.root-servers.net"] = "128.63.2.53" + rootservers["i.root-servers.net"] = "192.36.148.17" + rootservers["j.root-servers.net"] = "192.58.128.30" + rootservers["k.root-servers.net"] = "193.0.14.129" + rootservers["l.root-servers.net"] = "199.7.83.42" + rootservers["m.root-servers.net"] = "202.12.27.33" + + rootservers_v6 = {} + rootservers_v6["a.root-servers.net"] = "2001:503:ba3e::2:30" + rootservers_v6["f.root-servers.net"] = "2001:500:2f::f" + rootservers_v6["h.root-servers.net"] = "2001:500:1::803f:235" + rootservers_v6["j.root-servers.net"] = "2001:503:c27::2:30" + rootservers_v6["k.root-servers.net"] = "2001:7fd::1" + rootservers_v6["m.root-servers.net"] = "2001:dc3::35" + + container_dn = "DC=RootDNSServers,CN=MicrosoftDNS,%s,%s" % (prefix, domaindn) + + # Add DC=RootDNSServers,CN=MicrosoftDNS,<PREFIX>,<DOMAINDN> + msg = ldb.Message(ldb.Dn(samdb, container_dn)) + props = [] + props.append(ndr_pack(TypeProperty(zone_type=dnsp.DNS_ZONE_TYPE_CACHE))) + props.append(ndr_pack(AllowUpdateProperty(allow_update=dnsp.DNS_ZONE_UPDATE_OFF))) + props.append(ndr_pack(SecureTimeProperty())) + props.append(ndr_pack(NorefreshIntervalProperty())) + props.append(ndr_pack(RefreshIntervalProperty())) + props.append(ndr_pack(AgingStateProperty())) + props.append(ndr_pack(AgingEnabledTimeProperty())) + msg["objectClass"] = ["top", "dnsZone"] + msg["cn"] = ldb.MessageElement("Zone", ldb.FLAG_MOD_ADD, "cn") + msg["dNSProperty"] = ldb.MessageElement(props, ldb.FLAG_MOD_ADD, "dNSProperty") + samdb.add(msg) + + # Add DC=@,DC=RootDNSServers,CN=MicrosoftDNS,<PREFIX>,<DOMAINDN> + record = [] + for rserver in rootservers: + record.append(ndr_pack(NSRecord(rserver, serial=0, ttl=0, rank=dnsp.DNS_RANK_ROOT_HINT))) + + msg = ldb.Message(ldb.Dn(samdb, "DC=@,%s" % container_dn)) + msg["objectClass"] = ["top", "dnsNode"] + msg["dnsRecord"] = ldb.MessageElement(record, ldb.FLAG_MOD_ADD, "dnsRecord") + samdb.add(msg) + + # Add DC=<rootserver>,DC=RootDNSServers,CN=MicrosoftDNS,<PREFIX>,<DOMAINDN> + for rserver in rootservers: + record = [ndr_pack(ARecord(rootservers[rserver], serial=0, ttl=0, rank=dnsp.DNS_RANK_ROOT_HINT))] + # Add AAAA record as well (How does W2K* add IPv6 records?) + #if rserver in rootservers_v6: + # record.append(ndr_pack(AAAARecord(rootservers_v6[rserver], serial=0, ttl=0))) + msg = ldb.Message(ldb.Dn(samdb, "DC=%s,%s" % (rserver, container_dn))) + msg["objectClass"] = ["top", "dnsNode"] + msg["dnsRecord"] = ldb.MessageElement(record, ldb.FLAG_MOD_ADD, "dnsRecord") + samdb.add(msg) + +def add_at_record(samdb, container_dn, prefix, hostname, dnsdomain, hostip, hostip6): + + fqdn_hostname = "%s.%s" % (hostname, dnsdomain) + + at_records = [] + + # SOA record + at_soa_record = SOARecord(fqdn_hostname, "hostmaster.%s" % dnsdomain) + at_records.append(ndr_pack(at_soa_record)) + + # NS record + at_ns_record = NSRecord(fqdn_hostname) + at_records.append(ndr_pack(at_ns_record)) + + if hostip is not None: + # A record + at_a_record = ARecord(hostip) + at_records.append(ndr_pack(at_a_record)) + + if hostip6 is not None: + # AAAA record + at_aaaa_record = AAAARecord(hostip6) + at_records.append(ndr_pack(at_aaaa_record)) + + msg = ldb.Message(ldb.Dn(samdb, "DC=@,%s" % container_dn)) + msg["objectClass"] = ["top", "dnsNode"] + msg["dnsRecord"] = ldb.MessageElement(at_records, ldb.FLAG_MOD_ADD, "dnsRecord") + samdb.add(msg) + + +def add_srv_record(samdb, container_dn, prefix, host, port): + srv_record = SRVRecord(host, port) + msg = ldb.Message(ldb.Dn(samdb, "%s,%s" % (prefix, container_dn))) + msg["objectClass"] = ["top", "dnsNode"] + msg["dnsRecord"] = ldb.MessageElement(ndr_pack(srv_record), ldb.FLAG_MOD_ADD, "dnsRecord") + samdb.add(msg) + + +def add_ns_record(samdb, container_dn, prefix, host): + ns_record = NSRecord(host) + msg = ldb.Message(ldb.Dn(samdb, "%s,%s" % (prefix, container_dn))) + msg["objectClass"] = ["top", "dnsNode"] + msg["dnsRecord"] = ldb.MessageElement(ndr_pack(ns_record), ldb.FLAG_MOD_ADD, "dnsRecord") + samdb.add(msg) + + +def add_ns_glue_record(samdb, container_dn, prefix, host): + ns_record = NSRecord(host, rank=dnsp.DNS_RANK_NS_GLUE) + msg = ldb.Message(ldb.Dn(samdb, "%s,%s" % (prefix, container_dn))) + msg["objectClass"] = ["top", "dnsNode"] + msg["dnsRecord"] = ldb.MessageElement(ndr_pack(ns_record), ldb.FLAG_MOD_ADD, "dnsRecord") + samdb.add(msg) + + +def add_cname_record(samdb, container_dn, prefix, host): + cname_record = CNameRecord(host) + msg = ldb.Message(ldb.Dn(samdb, "%s,%s" % (prefix, container_dn))) + msg["objectClass"] = ["top", "dnsNode"] + msg["dnsRecord"] = ldb.MessageElement(ndr_pack(cname_record), ldb.FLAG_MOD_ADD, "dnsRecord") + samdb.add(msg) + + +def add_host_record(samdb, container_dn, prefix, hostip, hostip6): + host_records = [] + if hostip: + a_record = ARecord(hostip) + host_records.append(ndr_pack(a_record)) + if hostip6: + aaaa_record = AAAARecord(hostip6) + host_records.append(ndr_pack(aaaa_record)) + if host_records: + msg = ldb.Message(ldb.Dn(samdb, "%s,%s" % (prefix, container_dn))) + msg["objectClass"] = ["top", "dnsNode"] + msg["dnsRecord"] = ldb.MessageElement(host_records, ldb.FLAG_MOD_ADD, "dnsRecord") + samdb.add(msg) + + +def add_domain_record(samdb, domaindn, prefix, dnsdomain, domainsid, dnsadmins_sid): + # DC=<DNSDOMAIN>,CN=MicrosoftDNS,<PREFIX>,<DOMAINDN> + sddl = "O:SYG:BAD:AI" \ + "(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;DA)" \ + "(A;;CC;;;AU)" \ + "(A;;RPLCLORC;;;WD)" \ + "(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)" \ + "(A;CI;RPWPCRCCDCLCRCWOWDSDDTSW;;;ED)" \ + "(A;CIID;RPWPCRCCDCLCRCWOWDSDDTSW;;;%s)" \ + "(A;CIID;RPWPCRCCDCLCRCWOWDSDDTSW;;;ED)" \ + "(OA;CIID;RPWPCR;91e647de-d96f-4b70-9557-d63ff4f3ccd8;;PS)" \ + "(A;CIID;RPWPCRCCDCLCLORCWOWDSDDTSW;;;EA)" \ + "(A;CIID;LC;;;RU)" \ + "(A;CIID;RPWPCRCCLCLORCWOWDSDSW;;;BA)" \ + "S:AI" % dnsadmins_sid + sec = security.descriptor.from_sddl(sddl, domainsid) + props = [] + props.append(ndr_pack(TypeProperty())) + props.append(ndr_pack(AllowUpdateProperty())) + props.append(ndr_pack(SecureTimeProperty())) + props.append(ndr_pack(NorefreshIntervalProperty(norefresh_interval=168))) + props.append(ndr_pack(RefreshIntervalProperty(refresh_interval=168))) + props.append(ndr_pack(AgingStateProperty())) + props.append(ndr_pack(AgingEnabledTimeProperty())) + msg = ldb.Message(ldb.Dn(samdb, "DC=%s,CN=MicrosoftDNS,%s,%s" % (dnsdomain, prefix, domaindn))) + msg["objectClass"] = ["top", "dnsZone"] + msg["ntSecurityDescriptor"] = ldb.MessageElement(ndr_pack(sec), ldb.FLAG_MOD_ADD, + "nTSecurityDescriptor") + msg["dNSProperty"] = ldb.MessageElement(props, ldb.FLAG_MOD_ADD, "dNSProperty") + samdb.add(msg) + + +def add_msdcs_record(samdb, forestdn, prefix, dnsforest): + # DC=_msdcs.<DNSFOREST>,CN=MicrosoftDNS,<PREFIX>,<FORESTDN> + msg = ldb.Message(ldb.Dn(samdb, "DC=_msdcs.%s,CN=MicrosoftDNS,%s,%s" % + (dnsforest, prefix, forestdn))) + msg["objectClass"] = ["top", "dnsZone"] + samdb.add(msg) + + +def add_dc_domain_records(samdb, domaindn, prefix, site, dnsdomain, hostname, + hostip, hostip6): + + fqdn_hostname = "%s.%s" % (hostname, dnsdomain) + + # Set up domain container - DC=<DNSDOMAIN>,CN=MicrosoftDNS,<PREFIX>,<DOMAINDN> + domain_container_dn = ldb.Dn(samdb, "DC=%s,CN=MicrosoftDNS,%s,%s" % + (dnsdomain, prefix, domaindn)) + + # DC=@ record + add_at_record(samdb, domain_container_dn, "DC=@", hostname, dnsdomain, + hostip, hostip6) + + # DC=<HOSTNAME> record + add_host_record(samdb, domain_container_dn, "DC=%s" % hostname, hostip, + hostip6) + + # DC=_kerberos._tcp record + add_srv_record(samdb, domain_container_dn, "DC=_kerberos._tcp", + fqdn_hostname, 88) + + # DC=_kerberos._tcp.<SITENAME>._sites record + add_srv_record(samdb, domain_container_dn, "DC=_kerberos._tcp.%s._sites" % + site, fqdn_hostname, 88) + + # DC=_kerberos._udp record + add_srv_record(samdb, domain_container_dn, "DC=_kerberos._udp", + fqdn_hostname, 88) + + # DC=_kpasswd._tcp record + add_srv_record(samdb, domain_container_dn, "DC=_kpasswd._tcp", + fqdn_hostname, 464) + + # DC=_kpasswd._udp record + add_srv_record(samdb, domain_container_dn, "DC=_kpasswd._udp", + fqdn_hostname, 464) + + # DC=_ldap._tcp record + add_srv_record(samdb, domain_container_dn, "DC=_ldap._tcp", fqdn_hostname, + 389) + + # DC=_ldap._tcp.<SITENAME>._sites record + add_srv_record(samdb, domain_container_dn, "DC=_ldap._tcp.%s._sites" % + site, fqdn_hostname, 389) + + # FIXME: The number of SRV records depend on the various roles this DC has. + # _gc and _msdcs records are added if the we are the forest dc and not subdomain dc + # + # Assumption: current DC is GC and add all the entries + + # DC=_gc._tcp record + add_srv_record(samdb, domain_container_dn, "DC=_gc._tcp", fqdn_hostname, + 3268) + + # DC=_gc._tcp.<SITENAME>,_sites record + add_srv_record(samdb, domain_container_dn, "DC=_gc._tcp.%s._sites" % site, + fqdn_hostname, 3268) + + # DC=_msdcs record + add_ns_glue_record(samdb, domain_container_dn, "DC=_msdcs", fqdn_hostname) + + # FIXME: Following entries are added only if DomainDnsZones and ForestDnsZones partitions + # are created + # + # Assumption: Additional entries won't hurt on os_level = 2000 + + # DC=_ldap._tcp.<SITENAME>._sites.DomainDnsZones + add_srv_record(samdb, domain_container_dn, + "DC=_ldap._tcp.%s._sites.DomainDnsZones" % site, fqdn_hostname, + 389) + + # DC=_ldap._tcp.<SITENAME>._sites.ForestDnsZones + add_srv_record(samdb, domain_container_dn, + "DC=_ldap._tcp.%s._sites.ForestDnsZones" % site, fqdn_hostname, + 389) + + # DC=_ldap._tcp.DomainDnsZones + add_srv_record(samdb, domain_container_dn, "DC=_ldap._tcp.DomainDnsZones", + fqdn_hostname, 389) + + # DC=_ldap._tcp.ForestDnsZones + add_srv_record(samdb, domain_container_dn, "DC=_ldap._tcp.ForestDnsZones", + fqdn_hostname, 389) + + # DC=DomainDnsZones + add_host_record(samdb, domain_container_dn, "DC=DomainDnsZones", hostip, + hostip6) + + # DC=ForestDnsZones + add_host_record(samdb, domain_container_dn, "DC=ForestDnsZones", hostip, + hostip6) + + +def add_dc_msdcs_records(samdb, forestdn, prefix, site, dnsforest, hostname, + hostip, hostip6, domainguid, ntdsguid): + + fqdn_hostname = "%s.%s" % (hostname, dnsforest) + + # Set up forest container - DC=<DNSDOMAIN>,CN=MicrosoftDNS,<PREFIX>,<DOMAINDN> + forest_container_dn = ldb.Dn(samdb, "DC=_msdcs.%s,CN=MicrosoftDNS,%s,%s" % + (dnsforest, prefix, forestdn)) + + # DC=@ record + add_at_record(samdb, forest_container_dn, "DC=@", hostname, dnsforest, + None, None) + + # DC=_kerberos._tcp.dc record + add_srv_record(samdb, forest_container_dn, "DC=_kerberos._tcp.dc", + fqdn_hostname, 88) + + # DC=_kerberos._tcp.<SITENAME>._sites.dc record + add_srv_record(samdb, forest_container_dn, + "DC=_kerberos._tcp.%s._sites.dc" % site, fqdn_hostname, 88) + + # DC=_ldap._tcp.dc record + add_srv_record(samdb, forest_container_dn, "DC=_ldap._tcp.dc", + fqdn_hostname, 389) + + # DC=_ldap._tcp.<SITENAME>._sites.dc record + add_srv_record(samdb, forest_container_dn, "DC=_ldap._tcp.%s._sites.dc" % + site, fqdn_hostname, 389) + + # DC=_ldap._tcp.<SITENAME>._sites.gc record + add_srv_record(samdb, forest_container_dn, "DC=_ldap._tcp.%s._sites.gc" % + site, fqdn_hostname, 3268) + + # DC=_ldap._tcp.gc record + add_srv_record(samdb, forest_container_dn, "DC=_ldap._tcp.gc", + fqdn_hostname, 3268) + + # DC=_ldap._tcp.pdc record + add_srv_record(samdb, forest_container_dn, "DC=_ldap._tcp.pdc", + fqdn_hostname, 389) + + # DC=gc record + add_host_record(samdb, forest_container_dn, "DC=gc", hostip, hostip6) + + # DC=_ldap._tcp.<DOMAINGUID>.domains record + add_srv_record(samdb, forest_container_dn, + "DC=_ldap._tcp.%s.domains" % domainguid, fqdn_hostname, 389) + + # DC=<NTDSGUID> + add_cname_record(samdb, forest_container_dn, "DC=%s" % ntdsguid, + fqdn_hostname) + + +def secretsdb_setup_dns(secretsdb, names, private_dir, realm, + dnsdomain, dns_keytab_path, dnspass): + """Add DNS specific bits to a secrets database. + + :param secretsdb: Ldb Handle to the secrets database + :param names: Names shortcut + :param machinepass: Machine password + """ + try: + os.unlink(os.path.join(private_dir, dns_keytab_path)) + except OSError: + pass + + setup_ldb(secretsdb, setup_path("secrets_dns.ldif"), { + "REALM": realm, + "DNSDOMAIN": dnsdomain, + "DNS_KEYTAB": dns_keytab_path, + "DNSPASS_B64": b64encode(dnspass), + "HOSTNAME": names.hostname, + "DNSNAME" : '%s.%s' % ( + names.netbiosname.lower(), names.dnsdomain.lower()) + }) + + +def create_dns_dir(logger, paths): + """Write out a DNS zone file, from the info in the current database. + + :param logger: Logger object + :param paths: paths object + """ + dns_dir = os.path.dirname(paths.dns) + + try: + shutil.rmtree(dns_dir, True) + except OSError: + pass + + os.mkdir(dns_dir, 0770) + + if paths.bind_gid is not None: + try: + os.chown(dns_dir, -1, paths.bind_gid) + # chmod needed to cope with umask + os.chmod(dns_dir, 0770) + except OSError: + if not os.environ.has_key('SAMBA_SELFTEST'): + logger.error("Failed to chown %s to bind gid %u" % ( + dns_dir, paths.bind_gid)) + + +def create_zone_file(lp, logger, paths, targetdir, dnsdomain, + hostip, hostip6, hostname, realm, domainguid, + ntdsguid, site): + """Write out a DNS zone file, from the info in the current database. + + :param paths: paths object + :param dnsdomain: DNS Domain name + :param domaindn: DN of the Domain + :param hostip: Local IPv4 IP + :param hostip6: Local IPv6 IP + :param hostname: Local hostname + :param realm: Realm name + :param domainguid: GUID of the domain. + :param ntdsguid: GUID of the hosts nTDSDSA record. + """ + assert isinstance(domainguid, str) + + if hostip6 is not None: + hostip6_base_line = " IN AAAA " + hostip6 + hostip6_host_line = hostname + " IN AAAA " + hostip6 + gc_msdcs_ip6_line = "gc._msdcs IN AAAA " + hostip6 + else: + hostip6_base_line = "" + hostip6_host_line = "" + gc_msdcs_ip6_line = "" + + if hostip is not None: + hostip_base_line = " IN A " + hostip + hostip_host_line = hostname + " IN A " + hostip + gc_msdcs_ip_line = "gc._msdcs IN A " + hostip + else: + hostip_base_line = "" + hostip_host_line = "" + gc_msdcs_ip_line = "" + + # we need to freeze the zone while we update the contents + if targetdir is None: + rndc = ' '.join(lp.get("rndc command")) + os.system(rndc + " freeze " + lp.get("realm")) + + setup_file(setup_path("provision.zone"), paths.dns, { + "HOSTNAME": hostname, + "DNSDOMAIN": dnsdomain, + "REALM": realm, + "HOSTIP_BASE_LINE": hostip_base_line, + "HOSTIP_HOST_LINE": hostip_host_line, + "DOMAINGUID": domainguid, + "DATESTRING": time.strftime("%Y%m%d%H"), + "DEFAULTSITE": site, + "NTDSGUID": ntdsguid, + "HOSTIP6_BASE_LINE": hostip6_base_line, + "HOSTIP6_HOST_LINE": hostip6_host_line, + "GC_MSDCS_IP_LINE": gc_msdcs_ip_line, + "GC_MSDCS_IP6_LINE": gc_msdcs_ip6_line, + }) + + if paths.bind_gid is not None: + try: + os.chown(paths.dns, -1, paths.bind_gid) + # chmod needed to cope with umask + os.chmod(paths.dns, 0664) + except OSError: + if not os.environ.has_key('SAMBA_SELFTEST'): + logger.error("Failed to chown %s to bind gid %u" % ( + paths.dns, paths.bind_gid)) + + if targetdir is None: + os.system(rndc + " unfreeze " + lp.get("realm")) + + +def create_samdb_copy(samdb, logger, paths, names, domainsid, domainguid): + """Create a copy of samdb and give write permissions to named for dns partitions + """ + private_dir = paths.private_dir + samldb_dir = os.path.join(private_dir, "sam.ldb.d") + dns_dir = os.path.dirname(paths.dns) + dns_samldb_dir = os.path.join(dns_dir, "sam.ldb.d") + + # Find the partitions and corresponding filenames + partfile = {} + res = samdb.search(base="@PARTITION", scope=ldb.SCOPE_BASE, attrs=["partition"]) + for tmp in res[0]["partition"]: + (nc, fname) = tmp.split(':') + partfile[nc.upper()] = fname + + # Create empty domain partition + domaindn = names.domaindn.upper() + domainpart_file = os.path.join(dns_dir, partfile[domaindn]) + try: + os.mkdir(dns_samldb_dir) + file(domainpart_file, 'w').close() + + # Fill the basedn and @OPTION records in domain partition + dom_ldb = samba.Ldb(domainpart_file) + domainguid_line = "objectGUID: %s\n-" % domainguid + descr = b64encode(get_domain_descriptor(domainsid)) + setup_add_ldif(dom_ldb, setup_path("provision_basedn.ldif"), { + "DOMAINDN" : names.domaindn, + "DOMAINGUID" : domainguid_line, + "DOMAINSID" : str(domainsid), + "DESCRIPTOR" : descr}) + setup_add_ldif(dom_ldb, + setup_path("provision_basedn_options.ldif"), None) + except: + logger.error( + "Failed to setup database for BIND, AD based DNS cannot be used") + raise + del partfile[domaindn] + + # Link dns partitions and metadata + domainzonedn = "DC=DOMAINDNSZONES,%s" % names.domaindn.upper() + forestzonedn = "DC=FORESTDNSZONES,%s" % names.rootdn.upper() + domainzone_file = partfile[domainzonedn] + forestzone_file = partfile[forestzonedn] + metadata_file = "metadata.tdb" + try: + os.link(os.path.join(samldb_dir, metadata_file), + os.path.join(dns_samldb_dir, metadata_file)) + os.link(os.path.join(private_dir, domainzone_file), + os.path.join(dns_dir, domainzone_file)) + os.link(os.path.join(private_dir, forestzone_file), + os.path.join(dns_dir, forestzone_file)) + except OSError: + logger.error( + "Failed to setup database for BIND, AD based DNS cannot be used") + raise + del partfile[domainzonedn] + del partfile[forestzonedn] + + # Copy root, config, schema partitions (and any other if any) + # Since samdb is open in the current process, copy them in a child process + try: + tdb_copy(os.path.join(private_dir, "sam.ldb"), + os.path.join(dns_dir, "sam.ldb")) + for nc in partfile: + pfile = partfile[nc] + tdb_copy(os.path.join(private_dir, pfile), + os.path.join(dns_dir, pfile)) + except: + logger.error( + "Failed to setup database for BIND, AD based DNS cannot be used") + raise + + # Give bind read/write permissions dns partitions + if paths.bind_gid is not None: + try: + os.chown(samldb_dir, -1, paths.bind_gid) + os.chmod(samldb_dir, 0750) + + for dirname, dirs, files in os.walk(dns_dir): + for d in dirs: + dpath = os.path.join(dirname, d) + os.chown(dpath, -1, paths.bind_gid) + os.chmod(dpath, 0770) + for f in files: + if f.endswith('.ldb') or f.endswith('.tdb'): + fpath = os.path.join(dirname, f) + os.chown(fpath, -1, paths.bind_gid) + os.chmod(fpath, 0660) + except OSError: + if not os.environ.has_key('SAMBA_SELFTEST'): + logger.error( + "Failed to set permissions to sam.ldb* files, fix manually") + else: + if not os.environ.has_key('SAMBA_SELFTEST'): + logger.warning("""Unable to find group id for BIND, + set permissions to sam.ldb* files manually""") + + +def create_dns_update_list(lp, logger, paths): + """Write out a dns_update_list file""" + # note that we use no variable substitution on this file + # the substitution is done at runtime by samba_dnsupdate, samba_spnupdate + setup_file(setup_path("dns_update_list"), paths.dns_update_list, None) + setup_file(setup_path("spn_update_list"), paths.spn_update_list, None) + + +def create_named_conf(paths, realm, dnsdomain, dns_backend): + """Write out a file containing zone statements suitable for inclusion in a + named.conf file (including GSS-TSIG configuration). + + :param paths: all paths + :param realm: Realm name + :param dnsdomain: DNS Domain name + :param dns_backend: DNS backend type + :param keytab_name: File name of DNS keytab file + """ + + if dns_backend == "BIND9_FLATFILE": + setup_file(setup_path("named.conf"), paths.namedconf, { + "DNSDOMAIN": dnsdomain, + "REALM": realm, + "ZONE_FILE": paths.dns, + "REALM_WC": "*." + ".".join(realm.split(".")[1:]), + "NAMED_CONF": paths.namedconf, + "NAMED_CONF_UPDATE": paths.namedconf_update + }) + + setup_file(setup_path("named.conf.update"), paths.namedconf_update) + + elif dns_backend == "BIND9_DLZ": + setup_file(setup_path("named.conf.dlz"), paths.namedconf, { + "NAMED_CONF": paths.namedconf, + "MODULESDIR" : samba.param.modules_dir(), + }) + + +def create_named_txt(path, realm, dnsdomain, dnsname, private_dir, + keytab_name): + """Write out a file containing zone statements suitable for inclusion in a + named.conf file (including GSS-TSIG configuration). + + :param path: Path of the new named.conf file. + :param realm: Realm name + :param dnsdomain: DNS Domain name + :param private_dir: Path to private directory + :param keytab_name: File name of DNS keytab file + """ + setup_file(setup_path("named.txt"), path, { + "DNSDOMAIN": dnsdomain, + "DNSNAME" : dnsname, + "REALM": realm, + "DNS_KEYTAB": keytab_name, + "DNS_KEYTAB_ABS": os.path.join(private_dir, keytab_name), + "PRIVATE_DIR": private_dir + }) + + +def is_valid_dns_backend(dns_backend): + return dns_backend in ("BIND9_FLATFILE", "BIND9_DLZ", "SAMBA_INTERNAL", "NONE") + + +def is_valid_os_level(os_level): + return DS_DOMAIN_FUNCTION_2000 <= os_level <= DS_DOMAIN_FUNCTION_2008_R2 + + +def create_dns_legacy(samdb, domainsid, forestdn, dnsadmins_sid): + # Set up MicrosoftDNS container + add_dns_container(samdb, forestdn, "CN=System", domainsid, dnsadmins_sid) + # Add root servers + add_rootservers(samdb, forestdn, "CN=System") + + +def fill_dns_data_legacy(samdb, domainsid, forestdn, dnsdomain, site, hostname, + hostip, hostip6, dnsadmins_sid): + # Add domain record + add_domain_record(samdb, forestdn, "CN=System", dnsdomain, domainsid, + dnsadmins_sid) + + # Add DNS records for a DC in domain + add_dc_domain_records(samdb, forestdn, "CN=System", site, dnsdomain, + hostname, hostip, hostip6) + + +def create_dns_partitions(samdb, domainsid, names, domaindn, forestdn, + dnsadmins_sid): + # Set up additional partitions (DomainDnsZones, ForstDnsZones) + setup_dns_partitions(samdb, domainsid, domaindn, forestdn, + names.configdn, names.serverdn) + + # Set up MicrosoftDNS containers + add_dns_container(samdb, domaindn, "DC=DomainDnsZones", domainsid, + dnsadmins_sid) + add_dns_container(samdb, forestdn, "DC=ForestDnsZones", domainsid, + dnsadmins_sid, forest=True) + + +def fill_dns_data_partitions(samdb, domainsid, site, domaindn, forestdn, + dnsdomain, dnsforest, hostname, hostip, hostip6, + domainguid, ntdsguid, dnsadmins_sid, autofill=True): + """Fill data in various AD partitions + + :param samdb: LDB object connected to sam.ldb file + :param domainsid: Domain SID (as dom_sid object) + :param site: Site name to create hostnames in + :param domaindn: DN of the domain + :param forestdn: DN of the forest + :param dnsdomain: DNS name of the domain + :param dnsforest: DNS name of the forest + :param hostname: Host name of this DC + :param hostip: IPv4 addresses + :param hostip6: IPv6 addresses + :param domainguid: Domain GUID + :param ntdsguid: NTDS GUID + :param dnsadmins_sid: SID for DnsAdmins group + :param autofill: Create DNS records (using fixed template) + """ + + ##### Set up DC=DomainDnsZones,<DOMAINDN> + # Add rootserver records + add_rootservers(samdb, domaindn, "DC=DomainDnsZones") + + # Add domain record + add_domain_record(samdb, domaindn, "DC=DomainDnsZones", dnsdomain, + domainsid, dnsadmins_sid) + + # Add DNS records for a DC in domain + if autofill: + add_dc_domain_records(samdb, domaindn, "DC=DomainDnsZones", site, + dnsdomain, hostname, hostip, hostip6) + + ##### Set up DC=ForestDnsZones,<DOMAINDN> + # Add _msdcs record + add_msdcs_record(samdb, forestdn, "DC=ForestDnsZones", dnsforest) + + # Add DNS records for a DC in forest + if autofill: + add_dc_msdcs_records(samdb, forestdn, "DC=ForestDnsZones", site, + dnsforest, hostname, hostip, hostip6, + domainguid, ntdsguid) + + +def setup_ad_dns(samdb, secretsdb, domainsid, names, paths, lp, logger, + dns_backend, os_level, site, dnspass=None, hostip=None, hostip6=None, + targetdir=None): + """Provision DNS information (assuming GC role) + + :param samdb: LDB object connected to sam.ldb file + :param secretsdb: LDB object connected to secrets.ldb file + :param domainsid: Domain SID (as dom_sid object) + :param names: Names shortcut + :param paths: Paths shortcut + :param lp: Loadparm object + :param logger: Logger object + :param dns_backend: Type of DNS backend + :param os_level: Functional level (treated as os level) + :param site: Site to create hostnames in + :param dnspass: Password for bind's DNS account + :param hostip: IPv4 address + :param hostip6: IPv6 address + :param targetdir: Target directory for creating DNS-related files for BIND9 + """ + + if not is_valid_dns_backend(dns_backend): + raise Exception("Invalid dns backend: %r" % dns_backend) + + if not is_valid_os_level(os_level): + raise Exception("Invalid os level: %r" % os_level) + + if dns_backend == "NONE": + logger.info("No DNS backend set, not configuring DNS") + return + + # Add dns accounts (DnsAdmins, DnsUpdateProxy) in domain + logger.info("Adding DNS accounts") + add_dns_accounts(samdb, names.domaindn) + + # If dns_backend is BIND9_FLATFILE + # Populate only CN=MicrosoftDNS,CN=System,<FORESTDN> + # + # If dns_backend is SAMBA_INTERNAL or BIND9_DLZ + # Populate DNS partitions + + # If os_level < 2003 (DS_DOMAIN_FUNCTION_2000) + # All dns records are in CN=MicrosoftDNS,CN=System,<FORESTDN> + # + # If os_level >= 2003 (DS_DOMAIN_FUNCTION_2003, DS_DOMAIN_FUNCTION_2008, + # DS_DOMAIN_FUNCTION_2008_R2) + # Root server records are in CN=MicrosoftDNS,CN=System,<FORESTDN> + # Domain records are in CN=MicrosoftDNS,CN=System,<FORESTDN> + # Domain records are in CN=MicrosoftDNS,DC=DomainDnsZones,<DOMAINDN> + # Forest records are in CN=MicrosoftDNS,DC=ForestDnsZones,<FORESTDN> + domaindn = names.domaindn + forestdn = samdb.get_root_basedn().get_linearized() + + dnsdomain = names.dnsdomain.lower() + dnsforest = dnsdomain + + hostname = names.netbiosname.lower() + + dnsadmins_sid = get_dnsadmins_sid(samdb, domaindn) + domainguid = get_domainguid(samdb, domaindn) + + # Create CN=System + logger.info("Creating CN=MicrosoftDNS,CN=System,%s" % forestdn) + create_dns_legacy(samdb, domainsid, forestdn, dnsadmins_sid) + + if os_level == DS_DOMAIN_FUNCTION_2000: + # Populating legacy dns + logger.info("Populating CN=MicrosoftDNS,CN=System,%s" % forestdn) + fill_dns_data_legacy(samdb, domainsid, forestdn, dnsdomain, site, + hostname, hostip, hostip6, dnsadmins_sid) + + elif dns_backend in ("SAMBA_INTERNAL", "BIND9_DLZ") and \ + os_level >= DS_DOMAIN_FUNCTION_2003: + + # Create DNS partitions + logger.info("Creating DomainDnsZones and ForestDnsZones partitions") + create_dns_partitions(samdb, domainsid, names, domaindn, forestdn, + dnsadmins_sid) + + # Populating dns partitions + logger.info("Populating DomainDnsZones and ForestDnsZones partitions") + fill_dns_data_partitions(samdb, domainsid, site, domaindn, forestdn, + dnsdomain, dnsforest, hostname, hostip, hostip6, + domainguid, names.ntdsguid, dnsadmins_sid) + + if dns_backend.startswith("BIND9_"): + setup_bind9_dns(samdb, secretsdb, domainsid, names, paths, lp, logger, + dns_backend, os_level, site=site, dnspass=dnspass, hostip=hostip, + hostip6=hostip6, targetdir=targetdir) + + +def setup_bind9_dns(samdb, secretsdb, domainsid, names, paths, lp, logger, + dns_backend, os_level, site=None, dnspass=None, hostip=None, + hostip6=None, targetdir=None): + """Provision DNS information (assuming BIND9 backend in DC role) + + :param samdb: LDB object connected to sam.ldb file + :param secretsdb: LDB object connected to secrets.ldb file + :param domainsid: Domain SID (as dom_sid object) + :param names: Names shortcut + :param paths: Paths shortcut + :param lp: Loadparm object + :param logger: Logger object + :param dns_backend: Type of DNS backend + :param os_level: Functional level (treated as os level) + :param site: Site to create hostnames in + :param dnspass: Password for bind's DNS account + :param hostip: IPv4 address + :param hostip6: IPv6 address + :param targetdir: Target directory for creating DNS-related files for BIND9 + """ + + if (not is_valid_dns_backend(dns_backend) or + not dns_backend.startswith("BIND9_")): + raise Exception("Invalid dns backend: %r" % dns_backend) + + if not is_valid_os_level(os_level): + raise Exception("Invalid os level: %r" % os_level) + + domaindn = names.domaindn + + domainguid = get_domainguid(samdb, domaindn) + + secretsdb_setup_dns(secretsdb, names, + paths.private_dir, realm=names.realm, + dnsdomain=names.dnsdomain, + dns_keytab_path=paths.dns_keytab, dnspass=dnspass) + + create_dns_dir(logger, paths) + + if dns_backend == "BIND9_FLATFILE": + create_zone_file(lp, logger, paths, targetdir, site=site, + dnsdomain=names.dnsdomain, hostip=hostip, + hostip6=hostip6, hostname=names.hostname, + realm=names.realm, domainguid=domainguid, + ntdsguid=names.ntdsguid) + + if dns_backend == "BIND9_DLZ" and os_level >= DS_DOMAIN_FUNCTION_2003: + create_samdb_copy(samdb, logger, paths, names, domainsid, domainguid) + + create_named_conf(paths, realm=names.realm, + dnsdomain=names.dnsdomain, dns_backend=dns_backend) + + create_named_txt(paths.namedtxt, + realm=names.realm, dnsdomain=names.dnsdomain, + dnsname = "%s.%s" % (names.hostname, names.dnsdomain), + private_dir=paths.private_dir, + keytab_name=paths.dns_keytab) + logger.info("See %s for an example configuration include file for BIND", + paths.namedconf) + logger.info("and %s for further documentation required for secure DNS " + "updates", paths.namedtxt) diff --git a/python/samba/samba3/__init__.py b/python/samba/samba3/__init__.py new file mode 100644 index 00000000000..acccff4e296 --- /dev/null +++ b/python/samba/samba3/__init__.py @@ -0,0 +1,408 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007 +# +# 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/>. +# + +"""Support for reading Samba 3 data files.""" + +__docformat__ = "restructuredText" + +REGISTRY_VALUE_PREFIX = "SAMBA_REGVAL" +REGISTRY_DB_VERSION = 1 + +import os +import struct +import tdb + +import passdb +import param as s3param + + +def fetch_uint32(tdb, key): + try: + data = tdb[key] + except KeyError: + return None + assert len(data) == 4 + return struct.unpack("<L", data)[0] + + +def fetch_int32(tdb, key): + try: + data = tdb[key] + except KeyError: + return None + assert len(data) == 4 + return struct.unpack("<l", data)[0] + + +class TdbDatabase(object): + """Simple Samba 3 TDB database reader.""" + def __init__(self, file): + """Open a file. + + :param file: Path of the file to open. + """ + self.tdb = tdb.Tdb(file, flags=os.O_RDONLY) + self._check_version() + + def _check_version(self): + pass + + def close(self): + """Close resources associated with this object.""" + self.tdb.close() + + +class Registry(TdbDatabase): + """Simple read-only support for reading the Samba3 registry. + + :note: This object uses the same syntax for registry key paths as + Samba 3. This particular format uses forward slashes for key path + separators and abbreviations for the predefined key names. + e.g.: HKLM/Software/Bar. + """ + def __len__(self): + """Return the number of keys.""" + return len(self.keys()) + + def keys(self): + """Return list with all the keys.""" + return [k.rstrip("\x00") for k in self.tdb.iterkeys() if not k.startswith(REGISTRY_VALUE_PREFIX)] + + def subkeys(self, key): + """Retrieve the subkeys for the specified key. + + :param key: Key path. + :return: list with key names + """ + data = self.tdb.get("%s\x00" % key) + if data is None: + return [] + (num, ) = struct.unpack("<L", data[0:4]) + keys = data[4:].split("\0") + assert keys[-1] == "" + keys.pop() + assert len(keys) == num + return keys + + def values(self, key): + """Return a dictionary with the values set for a specific key. + + :param key: Key to retrieve values for. + :return: Dictionary with value names as key, tuple with type and + data as value.""" + data = self.tdb.get("%s/%s\x00" % (REGISTRY_VALUE_PREFIX, key)) + if data is None: + return {} + ret = {} + (num, ) = struct.unpack("<L", data[0:4]) + data = data[4:] + for i in range(num): + # Value name + (name, data) = data.split("\0", 1) + + (type, ) = struct.unpack("<L", data[0:4]) + data = data[4:] + (value_len, ) = struct.unpack("<L", data[0:4]) + data = data[4:] + + ret[name] = (type, data[:value_len]) + data = data[value_len:] + + return ret + + +# High water mark keys +IDMAP_HWM_GROUP = "GROUP HWM\0" +IDMAP_HWM_USER = "USER HWM\0" + +IDMAP_GROUP_PREFIX = "GID " +IDMAP_USER_PREFIX = "UID " + +# idmap version determines auto-conversion +IDMAP_VERSION_V2 = 2 + +class IdmapDatabase(TdbDatabase): + """Samba 3 ID map database reader.""" + + def _check_version(self): + assert fetch_int32(self.tdb, "IDMAP_VERSION\0") == IDMAP_VERSION_V2 + + def ids(self): + """Retrieve a list of all ids in this database.""" + for k in self.tdb.iterkeys(): + if k.startswith(IDMAP_USER_PREFIX): + yield k.rstrip("\0").split(" ") + if k.startswith(IDMAP_GROUP_PREFIX): + yield k.rstrip("\0").split(" ") + + def uids(self): + """Retrieve a list of all uids in this database.""" + for k in self.tdb.iterkeys(): + if k.startswith(IDMAP_USER_PREFIX): + yield int(k[len(IDMAP_USER_PREFIX):].rstrip("\0")) + + def gids(self): + """Retrieve a list of all gids in this database.""" + for k in self.tdb.iterkeys(): + if k.startswith(IDMAP_GROUP_PREFIX): + yield int(k[len(IDMAP_GROUP_PREFIX):].rstrip("\0")) + + def get_sid(self, xid, id_type): + """Retrive SID associated with a particular id and type. + + :param xid: UID or GID to retrive SID for. + :param id_type: Type of id specified - 'UID' or 'GID' + """ + data = self.tdb.get("%s %s\0" % (id_type, str(xid))) + if data is None: + return data + return data.rstrip("\0") + + def get_user_sid(self, uid): + """Retrieve the SID associated with a particular uid. + + :param uid: UID to retrieve SID for. + :return: A SID or None if no mapping was found. + """ + data = self.tdb.get("%s%d\0" % (IDMAP_USER_PREFIX, uid)) + if data is None: + return data + return data.rstrip("\0") + + def get_group_sid(self, gid): + data = self.tdb.get("%s%d\0" % (IDMAP_GROUP_PREFIX, gid)) + if data is None: + return data + return data.rstrip("\0") + + def get_user_hwm(self): + """Obtain the user high-water mark.""" + return fetch_uint32(self.tdb, IDMAP_HWM_USER) + + def get_group_hwm(self): + """Obtain the group high-water mark.""" + return fetch_uint32(self.tdb, IDMAP_HWM_GROUP) + + +class SecretsDatabase(TdbDatabase): + """Samba 3 Secrets database reader.""" + + def get_auth_password(self): + return self.tdb.get("SECRETS/AUTH_PASSWORD") + + def get_auth_domain(self): + return self.tdb.get("SECRETS/AUTH_DOMAIN") + + def get_auth_user(self): + return self.tdb.get("SECRETS/AUTH_USER") + + def get_domain_guid(self, host): + return self.tdb.get("SECRETS/DOMGUID/%s" % host) + + def ldap_dns(self): + for k in self.tdb.iterkeys(): + if k.startswith("SECRETS/LDAP_BIND_PW/"): + yield k[len("SECRETS/LDAP_BIND_PW/"):].rstrip("\0") + + def domains(self): + """Iterate over domains in this database. + + :return: Iterator over the names of domains in this database. + """ + for k in self.tdb.iterkeys(): + if k.startswith("SECRETS/SID/"): + yield k[len("SECRETS/SID/"):].rstrip("\0") + + def get_ldap_bind_pw(self, host): + return self.tdb.get("SECRETS/LDAP_BIND_PW/%s" % host) + + def get_afs_keyfile(self, host): + return self.tdb.get("SECRETS/AFS_KEYFILE/%s" % host) + + def get_machine_sec_channel_type(self, host): + return fetch_uint32(self.tdb, "SECRETS/MACHINE_SEC_CHANNEL_TYPE/%s" % host) + + def get_machine_last_change_time(self, host): + return fetch_uint32(self.tdb, "SECRETS/MACHINE_LAST_CHANGE_TIME/%s" % host) + + def get_machine_password(self, host): + return self.tdb.get("SECRETS/MACHINE_PASSWORD/%s" % host) + + def get_machine_acc(self, host): + return self.tdb.get("SECRETS/$MACHINE.ACC/%s" % host) + + def get_domtrust_acc(self, host): + return self.tdb.get("SECRETS/$DOMTRUST.ACC/%s" % host) + + def trusted_domains(self): + for k in self.tdb.iterkeys(): + if k.startswith("SECRETS/$DOMTRUST.ACC/"): + yield k[len("SECRETS/$DOMTRUST.ACC/"):].rstrip("\0") + + def get_random_seed(self): + return self.tdb.get("INFO/random_seed") + + def get_sid(self, host): + return self.tdb.get("SECRETS/SID/%s" % host.upper()) + + +SHARE_DATABASE_VERSION_V1 = 1 +SHARE_DATABASE_VERSION_V2 = 2 + + +class ShareInfoDatabase(TdbDatabase): + """Samba 3 Share Info database reader.""" + + def _check_version(self): + assert fetch_int32(self.tdb, "INFO/version\0") in (SHARE_DATABASE_VERSION_V1, SHARE_DATABASE_VERSION_V2) + + def get_secdesc(self, name): + """Obtain the security descriptor on a particular share. + + :param name: Name of the share + """ + secdesc = self.tdb.get("SECDESC/%s" % name) + # FIXME: Run ndr_pull_security_descriptor + return secdesc + + +class Shares(object): + """Container for share objects.""" + def __init__(self, lp, shareinfo): + self.lp = lp + self.shareinfo = shareinfo + + def __len__(self): + """Number of shares.""" + return len(self.lp) - 1 + + def __iter__(self): + """Iterate over the share names.""" + return self.lp.__iter__() + + +def shellsplit(text): + """Very simple shell-like line splitting. + + :param text: Text to split. + :return: List with parts of the line as strings. + """ + ret = list() + inquotes = False + current = "" + for c in text: + if c == "\"": + inquotes = not inquotes + elif c in ("\t", "\n", " ") and not inquotes: + if current != "": + ret.append(current) + current = "" + else: + current += c + if current != "": + ret.append(current) + return ret + + +class WinsDatabase(object): + """Samba 3 WINS database reader.""" + def __init__(self, file): + self.entries = {} + f = open(file, 'r') + assert f.readline().rstrip("\n") == "VERSION 1 0" + for l in f.readlines(): + if l[0] == "#": # skip comments + continue + entries = shellsplit(l.rstrip("\n")) + name = entries[0] + ttl = int(entries[1]) + i = 2 + ips = [] + while "." in entries[i]: + ips.append(entries[i]) + i+=1 + nb_flags = int(entries[i][:-1], 16) + assert not name in self.entries, "Name %s exists twice" % name + self.entries[name] = (ttl, ips, nb_flags) + f.close() + + def __getitem__(self, name): + return self.entries[name] + + def __len__(self): + return len(self.entries) + + def __iter__(self): + return iter(self.entries) + + def items(self): + """Return the entries in this WINS database.""" + return self.entries.items() + + def close(self): # for consistency + pass + + +class Samba3(object): + """Samba 3 configuration and state data reader.""" + + def __init__(self, smbconfpath, s3_lp_ctx=None): + """Open the configuration and data for a Samba 3 installation. + + :param smbconfpath: Path to the smb.conf file. + :param s3_lp_ctx: Samba3 Loadparm context + """ + self.smbconfpath = smbconfpath + if s3_lp_ctx: + self.lp = s3_lp_ctx + else: + self.lp = s3param.get_context() + self.lp.load(smbconfpath) + + def statedir_path(self, path): + if path[0] == "/" or path[0] == ".": + return path + return os.path.join(self.lp.get("state directory"), path) + + def privatedir_path(self, path): + if path[0] == "/" or path[0] == ".": + return path + return os.path.join(self.lp.get("private dir"), path) + + def get_conf(self): + return self.lp + + def get_sam_db(self): + return passdb.PDB(self.lp.get('passdb backend')) + + def get_registry(self): + return Registry(self.statedir_path("registry.tdb")) + + def get_secrets_db(self): + return SecretsDatabase(self.privatedir_path("secrets.tdb")) + + def get_shareinfo_db(self): + return ShareInfoDatabase(self.statedir_path("share_info.tdb")) + + def get_idmap_db(self): + return IdmapDatabase(self.statedir_path("winbindd_idmap.tdb")) + + def get_wins_db(self): + return WinsDatabase(self.statedir_path("wins.dat")) + + def get_shares(self): + return Shares(self.get_conf(), self.get_shareinfo_db()) diff --git a/python/samba/samdb.py b/python/samba/samdb.py new file mode 100644 index 00000000000..2dfc839519e --- /dev/null +++ b/python/samba/samdb.py @@ -0,0 +1,886 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2010 +# Copyright (C) Matthias Dieter Wallnoefer 2009 +# +# Based on the original in EJS: +# Copyright (C) Andrew Tridgell <tridge@samba.org> 2005 +# Copyright (C) Giampaolo Lauria <lauria2@yahoo.com> 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/>. +# + +"""Convenience functions for using the SAM.""" + +import samba +import ldb +import time +import base64 +import os +from samba import dsdb +from samba.ndr import ndr_unpack, ndr_pack +from samba.dcerpc import drsblobs, misc +from samba.common import normalise_int32 + +__docformat__ = "restructuredText" + + +class SamDB(samba.Ldb): + """The SAM database.""" + + hash_oid_name = {} + + def __init__(self, url=None, lp=None, modules_dir=None, session_info=None, + credentials=None, flags=0, options=None, global_schema=True, + auto_connect=True, am_rodc=None): + self.lp = lp + if not auto_connect: + url = None + elif url is None and lp is not None: + url = lp.samdb_url() + + self.url = url + + super(SamDB, self).__init__(url=url, lp=lp, modules_dir=modules_dir, + session_info=session_info, credentials=credentials, flags=flags, + options=options) + + if global_schema: + dsdb._dsdb_set_global_schema(self) + + if am_rodc is not None: + dsdb._dsdb_set_am_rodc(self, am_rodc) + + def connect(self, url=None, flags=0, options=None): + '''connect to the database''' + if self.lp is not None and not os.path.exists(url): + url = self.lp.private_path(url) + self.url = url + + super(SamDB, self).connect(url=url, flags=flags, + options=options) + + def am_rodc(self): + '''return True if we are an RODC''' + return dsdb._am_rodc(self) + + def am_pdc(self): + '''return True if we are an PDC emulator''' + return dsdb._am_pdc(self) + + def domain_dn(self): + '''return the domain DN''' + return str(self.get_default_basedn()) + + def disable_account(self, search_filter): + """Disables an account + + :param search_filter: LDAP filter to find the user (eg + samccountname=name) + """ + + flags = samba.dsdb.UF_ACCOUNTDISABLE + self.toggle_userAccountFlags(search_filter, flags, on=True) + + def enable_account(self, search_filter): + """Enables an account + + :param search_filter: LDAP filter to find the user (eg + samccountname=name) + """ + + flags = samba.dsdb.UF_ACCOUNTDISABLE | samba.dsdb.UF_PASSWD_NOTREQD + self.toggle_userAccountFlags(search_filter, flags, on=False) + + def toggle_userAccountFlags(self, search_filter, flags, flags_str=None, + on=True, strict=False): + """Toggle_userAccountFlags + + :param search_filter: LDAP filter to find the user (eg + samccountname=name) + :param flags: samba.dsdb.UF_* flags + :param on: on=True (default) => set, on=False => unset + :param strict: strict=False (default) ignore if no action is needed + strict=True raises an Exception if... + """ + res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE, + expression=search_filter, attrs=["userAccountControl"]) + if len(res) == 0: + raise Exception("Unable to find account where '%s'" % search_filter) + assert(len(res) == 1) + account_dn = res[0].dn + + old_uac = int(res[0]["userAccountControl"][0]) + if on: + if strict and (old_uac & flags): + error = "Account flag(s) '%s' already set" % flags_str + raise Exception(error) + + new_uac = old_uac | flags + else: + if strict and not (old_uac & flags): + error = "Account flag(s) '%s' already unset" % flags_str + raise Exception(error) + + new_uac = old_uac & ~flags + + if old_uac == new_uac: + return + + mod = """ +dn: %s +changetype: modify +delete: userAccountControl +userAccountControl: %u +add: userAccountControl +userAccountControl: %u +""" % (account_dn, old_uac, new_uac) + self.modify_ldif(mod) + + def force_password_change_at_next_login(self, search_filter): + """Forces a password change at next login + + :param search_filter: LDAP filter to find the user (eg + samccountname=name) + """ + res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE, + expression=search_filter, attrs=[]) + if len(res) == 0: + raise Exception('Unable to find user "%s"' % search_filter) + assert(len(res) == 1) + user_dn = res[0].dn + + mod = """ +dn: %s +changetype: modify +replace: pwdLastSet +pwdLastSet: 0 +""" % (user_dn) + self.modify_ldif(mod) + + def newgroup(self, groupname, groupou=None, grouptype=None, + description=None, mailaddress=None, notes=None, sd=None): + """Adds a new group with additional parameters + + :param groupname: Name of the new group + :param grouptype: Type of the new group + :param description: Description of the new group + :param mailaddress: Email address of the new group + :param notes: Notes of the new group + :param sd: security descriptor of the object + """ + + group_dn = "CN=%s,%s,%s" % (groupname, (groupou or "CN=Users"), self.domain_dn()) + + # The new user record. Note the reliance on the SAMLDB module which + # fills in the default informations + ldbmessage = {"dn": group_dn, + "sAMAccountName": groupname, + "objectClass": "group"} + + if grouptype is not None: + ldbmessage["groupType"] = normalise_int32(grouptype) + + if description is not None: + ldbmessage["description"] = description + + if mailaddress is not None: + ldbmessage["mail"] = mailaddress + + if notes is not None: + ldbmessage["info"] = notes + + if sd is not None: + ldbmessage["nTSecurityDescriptor"] = ndr_pack(sd) + + self.add(ldbmessage) + + def deletegroup(self, groupname): + """Deletes a group + + :param groupname: Name of the target group + """ + + groupfilter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (ldb.binary_encode(groupname), "CN=Group,CN=Schema,CN=Configuration", self.domain_dn()) + self.transaction_start() + try: + targetgroup = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE, + expression=groupfilter, attrs=[]) + if len(targetgroup) == 0: + raise Exception('Unable to find group "%s"' % groupname) + assert(len(targetgroup) == 1) + self.delete(targetgroup[0].dn) + except: + self.transaction_cancel() + raise + else: + self.transaction_commit() + + def add_remove_group_members(self, groupname, members, + add_members_operation=True): + """Adds or removes group members + + :param groupname: Name of the target group + :param members: list of group members + :param add_members_operation: Defines if its an add or remove + operation + """ + + groupfilter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % ( + ldb.binary_encode(groupname), "CN=Group,CN=Schema,CN=Configuration", self.domain_dn()) + + self.transaction_start() + try: + targetgroup = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE, + expression=groupfilter, attrs=['member']) + if len(targetgroup) == 0: + raise Exception('Unable to find group "%s"' % groupname) + assert(len(targetgroup) == 1) + + modified = False + + addtargettogroup = """ +dn: %s +changetype: modify +""" % (str(targetgroup[0].dn)) + + for member in members: + targetmember = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE, + expression="(|(sAMAccountName=%s)(CN=%s))" % ( + ldb.binary_encode(member), ldb.binary_encode(member)), attrs=[]) + + if len(targetmember) != 1: + continue + + if add_members_operation is True and (targetgroup[0].get('member') is None or str(targetmember[0].dn) not in targetgroup[0]['member']): + modified = True + addtargettogroup += """add: member +member: %s +""" % (str(targetmember[0].dn)) + + elif add_members_operation is False and (targetgroup[0].get('member') is not None and str(targetmember[0].dn) in targetgroup[0]['member']): + modified = True + addtargettogroup += """delete: member +member: %s +""" % (str(targetmember[0].dn)) + + if modified is True: + self.modify_ldif(addtargettogroup) + + except: + self.transaction_cancel() + raise + else: + self.transaction_commit() + + def newuser(self, username, password, + force_password_change_at_next_login_req=False, + useusernameascn=False, userou=None, surname=None, givenname=None, + initials=None, profilepath=None, scriptpath=None, homedrive=None, + homedirectory=None, jobtitle=None, department=None, company=None, + description=None, mailaddress=None, internetaddress=None, + telephonenumber=None, physicaldeliveryoffice=None, sd=None, + setpassword=True, uidnumber=None, gidnumber=None, gecos=None, + loginshell=None, uid=None): + """Adds a new user with additional parameters + + :param username: Name of the new user + :param password: Password for the new user + :param force_password_change_at_next_login_req: Force password change + :param useusernameascn: Use username as cn rather that firstname + + initials + lastname + :param userou: Object container (without domainDN postfix) for new user + :param surname: Surname of the new user + :param givenname: First name of the new user + :param initials: Initials of the new user + :param profilepath: Profile path of the new user + :param scriptpath: Logon script path of the new user + :param homedrive: Home drive of the new user + :param homedirectory: Home directory of the new user + :param jobtitle: Job title of the new user + :param department: Department of the new user + :param company: Company of the new user + :param description: of the new user + :param mailaddress: Email address of the new user + :param internetaddress: Home page of the new user + :param telephonenumber: Phone number of the new user + :param physicaldeliveryoffice: Office location of the new user + :param sd: security descriptor of the object + :param setpassword: optionally disable password reset + :param uidnumber: RFC2307 Unix numeric UID of the new user + :param gidnumber: RFC2307 Unix primary GID of the new user + :param gecos: RFC2307 Unix GECOS field of the new user + :param loginshell: RFC2307 Unix login shell of the new user + :param uid: RFC2307 Unix username of the new user + """ + + displayname = "" + if givenname is not None: + displayname += givenname + + if initials is not None: + displayname += ' %s.' % initials + + if surname is not None: + displayname += ' %s' % surname + + cn = username + if useusernameascn is None and displayname is not "": + cn = displayname + + user_dn = "CN=%s,%s,%s" % (cn, (userou or "CN=Users"), self.domain_dn()) + + dnsdomain = ldb.Dn(self, self.domain_dn()).canonical_str().replace("/", "") + user_principal_name = "%s@%s" % (username, dnsdomain) + # The new user record. Note the reliance on the SAMLDB module which + # fills in the default informations + ldbmessage = {"dn": user_dn, + "sAMAccountName": username, + "userPrincipalName": user_principal_name, + "objectClass": "user"} + + if surname is not None: + ldbmessage["sn"] = surname + + if givenname is not None: + ldbmessage["givenName"] = givenname + + if displayname is not "": + ldbmessage["displayName"] = displayname + ldbmessage["name"] = displayname + + if initials is not None: + ldbmessage["initials"] = '%s.' % initials + + if profilepath is not None: + ldbmessage["profilePath"] = profilepath + + if scriptpath is not None: + ldbmessage["scriptPath"] = scriptpath + + if homedrive is not None: + ldbmessage["homeDrive"] = homedrive + + if homedirectory is not None: + ldbmessage["homeDirectory"] = homedirectory + + if jobtitle is not None: + ldbmessage["title"] = jobtitle + + if department is not None: + ldbmessage["department"] = department + + if company is not None: + ldbmessage["company"] = company + + if description is not None: + ldbmessage["description"] = description + + if mailaddress is not None: + ldbmessage["mail"] = mailaddress + + if internetaddress is not None: + ldbmessage["wWWHomePage"] = internetaddress + + if telephonenumber is not None: + ldbmessage["telephoneNumber"] = telephonenumber + + if physicaldeliveryoffice is not None: + ldbmessage["physicalDeliveryOfficeName"] = physicaldeliveryoffice + + if sd is not None: + ldbmessage["nTSecurityDescriptor"] = ndr_pack(sd) + + ldbmessage2 = None + if any(map(lambda b: b is not None, (uid, uidnumber, gidnumber, gecos, loginshell))): + ldbmessage2 = ldb.Message() + ldbmessage2.dn = ldb.Dn(self, user_dn) + ldbmessage2["objectClass"] = ldb.MessageElement('posixAccount', ldb.FLAG_MOD_ADD, 'objectClass') + if uid is not None: + ldbmessage2["uid"] = ldb.MessageElement(str(uid), ldb.FLAG_MOD_REPLACE, 'uid') + if uidnumber is not None: + ldbmessage2["uidNumber"] = ldb.MessageElement(str(uidnumber), ldb.FLAG_MOD_REPLACE, 'uidNumber') + if gidnumber is not None: + ldbmessage2["gidNumber"] = ldb.MessageElement(str(gidnumber), ldb.FLAG_MOD_REPLACE, 'gidNumber') + if gecos is not None: + ldbmessage2["gecos"] = ldb.MessageElement(str(gecos), ldb.FLAG_MOD_REPLACE, 'gecos') + if loginshell is not None: + ldbmessage2["loginShell"] = ldb.MessageElement(str(loginshell), ldb.FLAG_MOD_REPLACE, 'loginShell') + + self.transaction_start() + try: + self.add(ldbmessage) + if ldbmessage2: + self.modify(ldbmessage2) + + # Sets the password for it + if setpassword: + self.setpassword("(samAccountName=%s)" % ldb.binary_encode(username), password, + force_password_change_at_next_login_req) + except: + self.transaction_cancel() + raise + else: + self.transaction_commit() + + + def deleteuser(self, username): + """Deletes a user + + :param username: Name of the target user + """ + + filter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (ldb.binary_encode(username), "CN=Person,CN=Schema,CN=Configuration", self.domain_dn()) + self.transaction_start() + try: + target = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE, + expression=filter, attrs=[]) + if len(target) == 0: + raise Exception('Unable to find user "%s"' % username) + assert(len(target) == 1) + self.delete(target[0].dn) + except: + self.transaction_cancel() + raise + else: + self.transaction_commit() + + def setpassword(self, search_filter, password, + force_change_at_next_login=False, username=None): + """Sets the password for a user + + :param search_filter: LDAP filter to find the user (eg + samccountname=name) + :param password: Password for the user + :param force_change_at_next_login: Force password change + """ + self.transaction_start() + try: + res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE, + expression=search_filter, attrs=[]) + if len(res) == 0: + raise Exception('Unable to find user "%s"' % (username or search_filter)) + if len(res) > 1: + raise Exception('Matched %u multiple users with filter "%s"' % (len(res), search_filter)) + user_dn = res[0].dn + pw = unicode('"' + password + '"', 'utf-8').encode('utf-16-le') + setpw = """ +dn: %s +changetype: modify +replace: unicodePwd +unicodePwd:: %s +""" % (user_dn, base64.b64encode(pw)) + + self.modify_ldif(setpw) + + if force_change_at_next_login: + self.force_password_change_at_next_login( + "(distinguishedName=" + str(user_dn) + ")") + + # modify the userAccountControl to remove the disabled bit + self.enable_account(search_filter) + except: + self.transaction_cancel() + raise + else: + self.transaction_commit() + + def setexpiry(self, search_filter, expiry_seconds, no_expiry_req=False): + """Sets the account expiry for a user + + :param search_filter: LDAP filter to find the user (eg + samaccountname=name) + :param expiry_seconds: expiry time from now in seconds + :param no_expiry_req: if set, then don't expire password + """ + self.transaction_start() + try: + res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE, + expression=search_filter, + attrs=["userAccountControl", "accountExpires"]) + if len(res) == 0: + raise Exception('Unable to find user "%s"' % search_filter) + assert(len(res) == 1) + user_dn = res[0].dn + + userAccountControl = int(res[0]["userAccountControl"][0]) + accountExpires = int(res[0]["accountExpires"][0]) + if no_expiry_req: + userAccountControl = userAccountControl | 0x10000 + accountExpires = 0 + else: + userAccountControl = userAccountControl & ~0x10000 + accountExpires = samba.unix2nttime(expiry_seconds + int(time.time())) + + setexp = """ +dn: %s +changetype: modify +replace: userAccountControl +userAccountControl: %u +replace: accountExpires +accountExpires: %u +""" % (user_dn, userAccountControl, accountExpires) + + self.modify_ldif(setexp) + except: + self.transaction_cancel() + raise + else: + self.transaction_commit() + + def set_domain_sid(self, sid): + """Change the domain SID used by this LDB. + + :param sid: The new domain sid to use. + """ + dsdb._samdb_set_domain_sid(self, sid) + + def get_domain_sid(self): + """Read the domain SID used by this LDB. """ + return dsdb._samdb_get_domain_sid(self) + + domain_sid = property(get_domain_sid, set_domain_sid, + "SID for the domain") + + def set_invocation_id(self, invocation_id): + """Set the invocation id for this SamDB handle. + + :param invocation_id: GUID of the invocation id. + """ + dsdb._dsdb_set_ntds_invocation_id(self, invocation_id) + + def get_invocation_id(self): + """Get the invocation_id id""" + return dsdb._samdb_ntds_invocation_id(self) + + invocation_id = property(get_invocation_id, set_invocation_id, + "Invocation ID GUID") + + def get_oid_from_attid(self, attid): + return dsdb._dsdb_get_oid_from_attid(self, attid) + + def get_attid_from_lDAPDisplayName(self, ldap_display_name, + is_schema_nc=False): + '''return the attribute ID for a LDAP attribute as an integer as found in DRSUAPI''' + return dsdb._dsdb_get_attid_from_lDAPDisplayName(self, + ldap_display_name, is_schema_nc) + + def get_syntax_oid_from_lDAPDisplayName(self, ldap_display_name): + '''return the syntax OID for a LDAP attribute as a string''' + return dsdb._dsdb_get_syntax_oid_from_lDAPDisplayName(self, ldap_display_name) + + def get_systemFlags_from_lDAPDisplayName(self, ldap_display_name): + '''return the systemFlags for a LDAP attribute as a integer''' + return dsdb._dsdb_get_systemFlags_from_lDAPDisplayName(self, ldap_display_name) + + def get_linkId_from_lDAPDisplayName(self, ldap_display_name): + '''return the linkID for a LDAP attribute as a integer''' + return dsdb._dsdb_get_linkId_from_lDAPDisplayName(self, ldap_display_name) + + def get_lDAPDisplayName_by_attid(self, attid): + '''return the lDAPDisplayName from an integer DRS attribute ID''' + return dsdb._dsdb_get_lDAPDisplayName_by_attid(self, attid) + + def get_backlink_from_lDAPDisplayName(self, ldap_display_name): + '''return the attribute name of the corresponding backlink from the name + of a forward link attribute. If there is no backlink return None''' + return dsdb._dsdb_get_backlink_from_lDAPDisplayName(self, ldap_display_name) + + def set_ntds_settings_dn(self, ntds_settings_dn): + """Set the NTDS Settings DN, as would be returned on the dsServiceName + rootDSE attribute. + + This allows the DN to be set before the database fully exists + + :param ntds_settings_dn: The new DN to use + """ + dsdb._samdb_set_ntds_settings_dn(self, ntds_settings_dn) + + def get_ntds_GUID(self): + """Get the NTDS objectGUID""" + return dsdb._samdb_ntds_objectGUID(self) + + def server_site_name(self): + """Get the server site name""" + return dsdb._samdb_server_site_name(self) + + def host_dns_name(self): + """return the DNS name of this host""" + res = self.search(base='', scope=ldb.SCOPE_BASE, attrs=['dNSHostName']) + return res[0]['dNSHostName'][0] + + def domain_dns_name(self): + """return the DNS name of the domain root""" + domain_dn = self.get_default_basedn() + return domain_dn.canonical_str().split('/')[0] + + def forest_dns_name(self): + """return the DNS name of the forest root""" + forest_dn = self.get_root_basedn() + return forest_dn.canonical_str().split('/')[0] + + def load_partition_usn(self, base_dn): + return dsdb._dsdb_load_partition_usn(self, base_dn) + + def set_schema(self, schema, write_indices_and_attributes=True): + self.set_schema_from_ldb(schema.ldb, write_indices_and_attributes=write_indices_and_attributes) + + def set_schema_from_ldb(self, ldb_conn, write_indices_and_attributes=True): + dsdb._dsdb_set_schema_from_ldb(self, ldb_conn, write_indices_and_attributes) + + def dsdb_DsReplicaAttribute(self, ldb, ldap_display_name, ldif_elements): + '''convert a list of attribute values to a DRSUAPI DsReplicaAttribute''' + return dsdb._dsdb_DsReplicaAttribute(ldb, ldap_display_name, ldif_elements) + + def dsdb_normalise_attributes(self, ldb, ldap_display_name, ldif_elements): + '''normalise a list of attribute values''' + return dsdb._dsdb_normalise_attributes(ldb, ldap_display_name, ldif_elements) + + def get_attribute_from_attid(self, attid): + """ Get from an attid the associated attribute + + :param attid: The attribute id for searched attribute + :return: The name of the attribute associated with this id + """ + if len(self.hash_oid_name.keys()) == 0: + self._populate_oid_attid() + if self.hash_oid_name.has_key(self.get_oid_from_attid(attid)): + return self.hash_oid_name[self.get_oid_from_attid(attid)] + else: + return None + + def _populate_oid_attid(self): + """Populate the hash hash_oid_name. + + This hash contains the oid of the attribute as a key and + its display name as a value + """ + self.hash_oid_name = {} + res = self.search(expression="objectClass=attributeSchema", + controls=["search_options:1:2"], + attrs=["attributeID", + "lDAPDisplayName"]) + if len(res) > 0: + for e in res: + strDisplay = str(e.get("lDAPDisplayName")) + self.hash_oid_name[str(e.get("attributeID"))] = strDisplay + + def get_attribute_replmetadata_version(self, dn, att): + """Get the version field trom the replPropertyMetaData for + the given field + + :param dn: The on which we want to get the version + :param att: The name of the attribute + :return: The value of the version field in the replPropertyMetaData + for the given attribute. None if the attribute is not replicated + """ + + res = self.search(expression="distinguishedName=%s" % dn, + scope=ldb.SCOPE_SUBTREE, + controls=["search_options:1:2"], + attrs=["replPropertyMetaData"]) + if len(res) == 0: + return None + + repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob, + str(res[0]["replPropertyMetaData"])) + ctr = repl.ctr + if len(self.hash_oid_name.keys()) == 0: + self._populate_oid_attid() + for o in ctr.array: + # Search for Description + att_oid = self.get_oid_from_attid(o.attid) + if self.hash_oid_name.has_key(att_oid) and\ + att.lower() == self.hash_oid_name[att_oid].lower(): + return o.version + return None + + def set_attribute_replmetadata_version(self, dn, att, value, + addifnotexist=False): + res = self.search(expression="distinguishedName=%s" % dn, + scope=ldb.SCOPE_SUBTREE, + controls=["search_options:1:2"], + attrs=["replPropertyMetaData"]) + if len(res) == 0: + return None + + repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob, + str(res[0]["replPropertyMetaData"])) + ctr = repl.ctr + now = samba.unix2nttime(int(time.time())) + found = False + if len(self.hash_oid_name.keys()) == 0: + self._populate_oid_attid() + for o in ctr.array: + # Search for Description + att_oid = self.get_oid_from_attid(o.attid) + if self.hash_oid_name.has_key(att_oid) and\ + att.lower() == self.hash_oid_name[att_oid].lower(): + found = True + seq = self.sequence_number(ldb.SEQ_NEXT) + o.version = value + o.originating_change_time = now + o.originating_invocation_id = misc.GUID(self.get_invocation_id()) + o.originating_usn = seq + o.local_usn = seq + + if not found and addifnotexist and len(ctr.array) >0: + o2 = drsblobs.replPropertyMetaData1() + o2.attid = 589914 + att_oid = self.get_oid_from_attid(o2.attid) + seq = self.sequence_number(ldb.SEQ_NEXT) + o2.version = value + o2.originating_change_time = now + o2.originating_invocation_id = misc.GUID(self.get_invocation_id()) + o2.originating_usn = seq + o2.local_usn = seq + found = True + tab = ctr.array + tab.append(o2) + ctr.count = ctr.count + 1 + ctr.array = tab + + if found : + replBlob = ndr_pack(repl) + msg = ldb.Message() + msg.dn = res[0].dn + msg["replPropertyMetaData"] = ldb.MessageElement(replBlob, + ldb.FLAG_MOD_REPLACE, + "replPropertyMetaData") + self.modify(msg, ["local_oid:1.3.6.1.4.1.7165.4.3.14:0"]) + + def write_prefixes_from_schema(self): + dsdb._dsdb_write_prefixes_from_schema_to_ldb(self) + + def get_partitions_dn(self): + return dsdb._dsdb_get_partitions_dn(self) + + def get_nc_root(self, dn): + return dsdb._dsdb_get_nc_root(self, dn) + + def get_wellknown_dn(self, nc_root, wkguid): + return dsdb._dsdb_get_wellknown_dn(self, nc_root, wkguid) + + def set_minPwdAge(self, value): + m = ldb.Message() + m.dn = ldb.Dn(self, self.domain_dn()) + m["minPwdAge"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "minPwdAge") + self.modify(m) + + def get_minPwdAge(self): + res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["minPwdAge"]) + if len(res) == 0: + return None + elif not "minPwdAge" in res[0]: + return None + else: + return res[0]["minPwdAge"][0] + + def set_minPwdLength(self, value): + m = ldb.Message() + m.dn = ldb.Dn(self, self.domain_dn()) + m["minPwdLength"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "minPwdLength") + self.modify(m) + + def get_minPwdLength(self): + res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["minPwdLength"]) + if len(res) == 0: + return None + elif not "minPwdLength" in res[0]: + return None + else: + return res[0]["minPwdLength"][0] + + def set_pwdProperties(self, value): + m = ldb.Message() + m.dn = ldb.Dn(self, self.domain_dn()) + m["pwdProperties"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "pwdProperties") + self.modify(m) + + def get_pwdProperties(self): + res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["pwdProperties"]) + if len(res) == 0: + return None + elif not "pwdProperties" in res[0]: + return None + else: + return res[0]["pwdProperties"][0] + + def set_dsheuristics(self, dsheuristics): + m = ldb.Message() + m.dn = ldb.Dn(self, "CN=Directory Service,CN=Windows NT,CN=Services,%s" + % self.get_config_basedn().get_linearized()) + if dsheuristics is not None: + m["dSHeuristics"] = ldb.MessageElement(dsheuristics, + ldb.FLAG_MOD_REPLACE, "dSHeuristics") + else: + m["dSHeuristics"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE, + "dSHeuristics") + self.modify(m) + + def get_dsheuristics(self): + res = self.search("CN=Directory Service,CN=Windows NT,CN=Services,%s" + % self.get_config_basedn().get_linearized(), + scope=ldb.SCOPE_BASE, attrs=["dSHeuristics"]) + if len(res) == 0: + dsheuristics = None + elif "dSHeuristics" in res[0]: + dsheuristics = res[0]["dSHeuristics"][0] + else: + dsheuristics = None + + return dsheuristics + + def create_ou(self, ou_dn, description=None, name=None, sd=None): + """Creates an organizationalUnit object + :param ou_dn: dn of the new object + :param description: description attribute + :param name: name atttribute + :param sd: security descriptor of the object, can be + an SDDL string or security.descriptor type + """ + m = {"dn": ou_dn, + "objectClass": "organizationalUnit"} + + if description: + m["description"] = description + if name: + m["name"] = name + + if sd: + m["nTSecurityDescriptor"] = ndr_pack(sd) + self.add(m) + + def sequence_number(self, seq_type): + """Returns the value of the sequence number according to the requested type + :param seq_type: type of sequence number + """ + self.transaction_start() + try: + seq = super(SamDB, self).sequence_number(seq_type) + except: + self.transaction_cancel() + raise + else: + self.transaction_commit() + return seq + + def get_dsServiceName(self): + '''get the NTDS DN from the rootDSE''' + res = self.search(base="", scope=ldb.SCOPE_BASE, attrs=["dsServiceName"]) + return res[0]["dsServiceName"][0] + + def get_serverName(self): + '''get the server DN from the rootDSE''' + res = self.search(base="", scope=ldb.SCOPE_BASE, attrs=["serverName"]) + return res[0]["serverName"][0] diff --git a/python/samba/schema.py b/python/samba/schema.py new file mode 100644 index 00000000000..5c8f506f26e --- /dev/null +++ b/python/samba/schema.py @@ -0,0 +1,204 @@ +# +# Unix SMB/CIFS implementation. +# backend code for provisioning a Samba4 server +# +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2008 +# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2008-2009 +# Copyright (C) Oliver Liebel <oliver@itc.li> 2008-2009 +# +# 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/>. +# + +"""Functions for setting up a Samba Schema.""" + +from base64 import b64encode +from samba import read_and_sub_file, substitute_var, check_all_substituted +from samba.dcerpc import security +from samba.ms_schema import read_ms_schema +from samba.ndr import ndr_pack +from samba.samdb import SamDB +from samba import dsdb +from ldb import SCOPE_SUBTREE, SCOPE_ONELEVEL +import os + +def get_schema_descriptor(domain_sid, name_map={}): + sddl = "O:SAG:SAD:AI(OA;;CR;e12b56b6-0a95-11d1-adbb-00c04fd8d5cd;;SA)" \ + "(OA;;CR;1131f6aa-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \ + "(OA;;CR;1131f6ab-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \ + "(OA;;CR;1131f6ac-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \ + "(OA;;CR;1131f6aa-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(OA;;CR;1131f6ab-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(OA;;CR;1131f6ac-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(A;CI;RPLCLORC;;;AU)" \ + "(A;CI;RPWPCRCCLCLORCWOWDSW;;;SA)" \ + "(A;CI;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)" \ + "(OA;;CR;1131f6ad-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \ + "(OA;;CR;89e95b76-444d-4c62-991a-0facbeda640c;;ED)" \ + "(OA;;CR;1131f6ad-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(OA;;CR;89e95b76-444d-4c62-991a-0facbeda640c;;BA)" \ + "(OA;;CR;1131f6aa-9c07-11d1-f79f-00c04fc2dcd2;;ER)" \ + "(OA;;CR;1131f6ad-9c07-11d1-f79f-00c04fc2dcd2;;ER)" \ + "(OA;;CR;89e95b76-444d-4c62-991a-0facbeda640c;;ER)" \ + "S:(AU;SA;WPCCDCWOWDSDDTSW;;;WD)" \ + "(AU;CISA;WP;;;WD)" \ + "(AU;SA;CR;;;BA)" \ + "(AU;SA;CR;;;DU)" \ + "(OU;SA;CR;e12b56b6-0a95-11d1-adbb-00c04fd8d5cd;;WD)" \ + "(OU;SA;CR;45ec5156-db7e-47bb-b53f-dbeb2d03c40f;;WD)" + sec = security.descriptor.from_sddl(sddl, domain_sid) + return ndr_pack(sec) + + +class Schema(object): + + def __init__(self, domain_sid, invocationid=None, schemadn=None, + files=None, override_prefixmap=None, additional_prefixmap=None): + from samba.provision import setup_path + + """Load schema for the SamDB from the AD schema files and + samba4_schema.ldif + + :param samdb: Load a schema into a SamDB. + :param schemadn: DN of the schema + + Returns the schema data loaded, to avoid double-parsing when then + needing to add it to the db + """ + + self.schemadn = schemadn + # We need to have the am_rodc=False just to keep some warnings quiet - + # this isn't a real SAM, so it's meaningless. + self.ldb = SamDB(global_schema=False, am_rodc=False) + if invocationid is not None: + self.ldb.set_invocation_id(invocationid) + + self.schema_data = read_ms_schema( + setup_path('ad-schema/MS-AD_Schema_2K8_R2_Attributes.txt'), + setup_path('ad-schema/MS-AD_Schema_2K8_R2_Classes.txt')) + + if files is not None: + for file in files: + self.schema_data += open(file, 'r').read() + + self.schema_data = substitute_var(self.schema_data, + {"SCHEMADN": schemadn}) + check_all_substituted(self.schema_data) + + self.schema_dn_modify = read_and_sub_file( + setup_path("provision_schema_basedn_modify.ldif"), + {"SCHEMADN": schemadn}) + + descr = b64encode(get_schema_descriptor(domain_sid)) + self.schema_dn_add = read_and_sub_file( + setup_path("provision_schema_basedn.ldif"), + {"SCHEMADN": schemadn, "DESCRIPTOR": descr}) + + if override_prefixmap is not None: + self.prefixmap_data = override_prefixmap + else: + self.prefixmap_data = open(setup_path("prefixMap.txt"), 'r').read() + + if additional_prefixmap is not None: + for map in additional_prefixmap: + self.prefixmap_data += "%s\n" % map + + self.prefixmap_data = b64encode(self.prefixmap_data) + + # We don't actually add this ldif, just parse it + prefixmap_ldif = "dn: %s\nprefixMap:: %s\n\n" % (self.schemadn, self.prefixmap_data) + self.set_from_ldif(prefixmap_ldif, self.schema_data, self.schemadn) + + def set_from_ldif(self, pf, df, dn): + dsdb._dsdb_set_schema_from_ldif(self.ldb, pf, df, dn) + + def write_to_tmp_ldb(self, schemadb_path): + self.ldb.connect(url=schemadb_path) + self.ldb.transaction_start() + try: + self.ldb.add_ldif("""dn: @ATTRIBUTES +linkID: INTEGER + +dn: @INDEXLIST +@IDXATTR: linkID +@IDXATTR: attributeSyntax +""") + # These bits of LDIF are supplied when the Schema object is created + self.ldb.add_ldif(self.schema_dn_add) + self.ldb.modify_ldif(self.schema_dn_modify) + self.ldb.add_ldif(self.schema_data) + except: + self.ldb.transaction_cancel() + raise + else: + self.ldb.transaction_commit() + + # Return a hash with the forward attribute as a key and the back as the + # value + def linked_attributes(self): + return get_linked_attributes(self.schemadn, self.ldb) + + def dnsyntax_attributes(self): + return get_dnsyntax_attributes(self.schemadn, self.ldb) + + def convert_to_openldap(self, target, mapping): + return dsdb._dsdb_convert_schema_to_openldap(self.ldb, target, mapping) + + +# Return a hash with the forward attribute as a key and the back as the value +def get_linked_attributes(schemadn,schemaldb): + attrs = ["linkID", "lDAPDisplayName"] + res = schemaldb.search(expression="(&(linkID=*)(!(linkID:1.2.840.113556.1.4.803:=1))(objectclass=attributeSchema)(attributeSyntax=2.5.5.1))", base=schemadn, scope=SCOPE_ONELEVEL, attrs=attrs) + attributes = {} + for i in range (0, len(res)): + expression = "(&(objectclass=attributeSchema)(linkID=%d)(attributeSyntax=2.5.5.1))" % (int(res[i]["linkID"][0])+1) + target = schemaldb.searchone(basedn=schemadn, + expression=expression, + attribute="lDAPDisplayName", + scope=SCOPE_SUBTREE) + if target is not None: + attributes[str(res[i]["lDAPDisplayName"])]=str(target) + + return attributes + + +def get_dnsyntax_attributes(schemadn,schemaldb): + res = schemaldb.search( + expression="(&(!(linkID=*))(objectclass=attributeSchema)(attributeSyntax=2.5.5.1))", + base=schemadn, scope=SCOPE_ONELEVEL, + attrs=["linkID", "lDAPDisplayName"]) + attributes = [] + for i in range (0, len(res)): + attributes.append(str(res[i]["lDAPDisplayName"])) + return attributes + + +def ldb_with_schema(schemadn="cn=schema,cn=configuration,dc=example,dc=com", + domainsid=None, + override_prefixmap=None): + """Load schema for the SamDB from the AD schema files and samba4_schema.ldif + + :param schemadn: DN of the schema + :param serverdn: DN of the server + + Returns the schema data loaded as an object, with .ldb being a + new ldb with the schema loaded. This allows certain tests to + operate without a remote or local schema. + """ + + if domainsid is None: + domainsid = security.random_sid() + else: + domainsid = security.dom_sid(domainsid) + return Schema(domainsid, schemadn=schemadn, + override_prefixmap=override_prefixmap) diff --git a/python/samba/sd_utils.py b/python/samba/sd_utils.py new file mode 100644 index 00000000000..ded9bfc1926 --- /dev/null +++ b/python/samba/sd_utils.py @@ -0,0 +1,80 @@ +# Utility methods for security descriptor manipulation +# +# Copyright Nadezhda Ivanova 2010 <nivanova@samba.org> +# +# 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/>. +# + +"""Utility methods for security descriptor manipulation.""" + +import samba +from ldb import Message, MessageElement, Dn +from ldb import FLAG_MOD_REPLACE, SCOPE_BASE +from samba.ndr import ndr_pack, ndr_unpack +from samba.dcerpc import security + + +class SDUtils(object): + """Some utilities for manipulation of security descriptors on objects.""" + + def __init__(self, samdb): + self.ldb = samdb + self.domain_sid = security.dom_sid(self.ldb.get_domain_sid()) + + def modify_sd_on_dn(self, object_dn, sd, controls=None): + """Modify security descriptor using either SDDL string + or security.descriptor object + """ + m = Message() + m.dn = Dn(self.ldb, object_dn) + assert(isinstance(sd, str) or isinstance(sd, security.descriptor)) + if isinstance(sd, str): + tmp_desc = security.descriptor.from_sddl(sd, self.domain_sid) + elif isinstance(sd, security.descriptor): + tmp_desc = sd + + m["nTSecurityDescriptor"] = MessageElement(ndr_pack(tmp_desc), + FLAG_MOD_REPLACE, + "nTSecurityDescriptor") + self.ldb.modify(m, controls) + + def read_sd_on_dn(self, object_dn, controls=None): + res = self.ldb.search(object_dn, SCOPE_BASE, None, + ["nTSecurityDescriptor"], controls=controls) + desc = res[0]["nTSecurityDescriptor"][0] + return ndr_unpack(security.descriptor, desc) + + def get_object_sid(self, object_dn): + res = self.ldb.search(object_dn) + return ndr_unpack(security.dom_sid, res[0]["objectSid"][0]) + + def dacl_add_ace(self, object_dn, ace): + """Add an ACE to an objects security descriptor + """ + desc = self.read_sd_on_dn(object_dn) + desc_sddl = desc.as_sddl(self.domain_sid) + if ace in desc_sddl: + return + if desc_sddl.find("(") >= 0: + desc_sddl = (desc_sddl[:desc_sddl.index("(")] + ace + + desc_sddl[desc_sddl.index("("):]) + else: + desc_sddl = desc_sddl + ace + self.modify_sd_on_dn(object_dn, desc_sddl) + + def get_sd_as_sddl(self, object_dn, controls=None): + """Return object nTSecutiryDescriptor in SDDL format + """ + desc = self.read_sd_on_dn(object_dn, controls=controls) + return desc.as_sddl(self.domain_sid) diff --git a/python/samba/sites.py b/python/samba/sites.py new file mode 100644 index 00000000000..76c57dd11cf --- /dev/null +++ b/python/samba/sites.py @@ -0,0 +1,125 @@ +# python site manipulation code +# Copyright Matthieu Patou <mat@matws.net> 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/>. +# + +"""Manipulating sites.""" + +import ldb +from ldb import FLAG_MOD_ADD + + +class SiteException(Exception): + """Base element for Sites errors""" + + def __init__(self, value): + self.value = value + + def __str__(self): + return "SiteException: " + self.value + + +class SiteNotFoundException(SiteException): + """Raised when the site is not found and it's expected to exists.""" + + def __init__(self, value): + self.value = value + + def __str__(self): + return "SiteNotFoundException: " + self.value + +class SiteAlreadyExistsException(SiteException): + """Raised when the site is not found and it's expected not to exists.""" + + def __init__(self, value): + self.value = value + + def __str__(self): + return "SiteAlreadyExists: " + self.value + +class SiteServerNotEmptyException(SiteException): + """Raised when the site still has servers attached.""" + + def __init__(self, value): + self.value = value + + def __str__(self): + return "SiteServerNotEmpty: " + self.value + +def create_site(samdb, configDn, siteName): + """ + Create a site + + :param samdb: A samdb connection + :param configDn: The DN of the configuration partition + :param siteName: Name of the site to create + :return: True upon success + :raise SiteAlreadyExists: if the site to be created already exists. + """ + + ret = samdb.search(base=configDn, scope=ldb.SCOPE_SUBTREE, + expression='(&(objectclass=Site)(cn=%s))' % siteName) + if len(ret) != 0: + raise SiteAlreadyExistsException('A site with the name %s already exists' % siteName) + + m = ldb.Message() + m.dn = ldb.Dn(samdb, "Cn=%s,CN=Sites,%s" % (siteName, str(configDn))) + m["objectclass"] = ldb.MessageElement("site", FLAG_MOD_ADD, "objectclass") + + samdb.add(m) + + m2 = ldb.Message() + m2.dn = ldb.Dn(samdb, "Cn=NTDS Site Settings,%s" % str(m.dn)) + m2["objectclass"] = ldb.MessageElement("nTDSSiteSettings", FLAG_MOD_ADD, "objectclass") + + samdb.add(m2) + + m3 = ldb.Message() + m3.dn = ldb.Dn(samdb, "Cn=Servers,%s" % str(m.dn)) + m3["objectclass"] = ldb.MessageElement("serversContainer", FLAG_MOD_ADD, "objectclass") + + samdb.add(m3) + + return True + +def delete_site(samdb, configDn, siteName): + """ + Delete a site + + :param samdb: A samdb connection + :param configDn: The DN of the configuration partition + :param siteName: Name of the site to delete + :return: True upon success + :raise SiteNotFoundException: if the site to be deleted do not exists. + :raise SiteServerNotEmpty: if the site has still servers in it. + """ + + dnsites = ldb.Dn(samdb, "CN=Sites,%s" % (str(configDn))) + dnsite = ldb.Dn(samdb, "Cn=%s,CN=Sites,%s" % (siteName, str(configDn))) + dnserver = ldb.Dn(samdb, "Cn=Servers,%s" % str(dnsite)) + + ret = samdb.search(base=dnsites, scope=ldb.SCOPE_ONELEVEL, + expression='(dn=%s)' % str(dnsite)) + if len(ret) != 1: + raise SiteNotFoundException('Site %s do not exists' % siteName) + + ret = samdb.search(base=dnserver, scope=ldb.SCOPE_ONELEVEL, + expression='(objectclass=server)') + if len(ret) != 0: + raise SiteServerNotEmptyException('Site %s still has servers in it, move them before removal' % siteName) + + samdb.delete(dnsite, ["tree_delete:0"]) + + return True diff --git a/python/samba/tdb_util.py b/python/samba/tdb_util.py new file mode 100644 index 00000000000..d967434e5b8 --- /dev/null +++ b/python/samba/tdb_util.py @@ -0,0 +1,41 @@ +# Unix SMB/CIFS implementation. +# tdb util helpers +# +# Copyright (C) Kai Blin <kai@samba.org> 2011 +# Copyright (C) Amitay Isaacs <amitay@gmail.com> 2011 +# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2013 +# +# 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 samba +import subprocess +import os + +def tdb_copy(file1, file2): + """Copy tdb file using tdbbackup utility and rename it + """ + # Find the location of tdbbackup tool + dirs = ["bin", samba.param.bin_dir()] + os.getenv('PATH').split(os.pathsep) + for d in dirs: + toolpath = os.path.join(d, "tdbbackup") + if os.path.exists(toolpath): + break + + tdbbackup_cmd = [toolpath, "-s", ".copy.tdb", file1] + status = subprocess.call(tdbbackup_cmd, close_fds=True, shell=False) + + if status == 0: + os.rename("%s.copy.tdb" % file1, file2) + else: + raise Exception("Error copying %s" % file1) diff --git a/python/samba/tests/__init__.py b/python/samba/tests/__init__.py new file mode 100644 index 00000000000..2df30a641bf --- /dev/null +++ b/python/samba/tests/__init__.py @@ -0,0 +1,237 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2010 +# +# 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/>. +# + +"""Samba Python tests.""" + +import os +import ldb +import samba +import samba.auth +from samba import param +from samba.samdb import SamDB +import subprocess +import tempfile + +samba.ensure_external_module("testtools", "testtools") + +# Other modules import these two classes from here, for convenience: +from testtools.testcase import ( + TestCase as TesttoolsTestCase, + TestSkipped, + ) + + +class TestCase(TesttoolsTestCase): + """A Samba test case.""" + + def setUp(self): + super(TestCase, self).setUp() + test_debug_level = os.getenv("TEST_DEBUG_LEVEL") + if test_debug_level is not None: + test_debug_level = int(test_debug_level) + self._old_debug_level = samba.get_debug_level() + samba.set_debug_level(test_debug_level) + self.addCleanup(samba.set_debug_level, test_debug_level) + + def get_loadparm(self): + return env_loadparm() + + def get_credentials(self): + return cmdline_credentials + + +class LdbTestCase(TesttoolsTestCase): + """Trivial test case for running tests against a LDB.""" + + def setUp(self): + super(LdbTestCase, self).setUp() + self.filename = os.tempnam() + self.ldb = samba.Ldb(self.filename) + + def set_modules(self, modules=[]): + """Change the modules for this Ldb.""" + m = ldb.Message() + m.dn = ldb.Dn(self.ldb, "@MODULES") + m["@LIST"] = ",".join(modules) + self.ldb.add(m) + self.ldb = samba.Ldb(self.filename) + + +class TestCaseInTempDir(TestCase): + + def setUp(self): + super(TestCaseInTempDir, self).setUp() + self.tempdir = tempfile.mkdtemp() + self.addCleanup(self._remove_tempdir) + + def _remove_tempdir(self): + self.assertEquals([], os.listdir(self.tempdir)) + os.rmdir(self.tempdir) + self.tempdir = None + + +def env_loadparm(): + lp = param.LoadParm() + try: + lp.load(os.environ["SMB_CONF_PATH"]) + except KeyError: + raise Exception("SMB_CONF_PATH not set") + return lp + + +def env_get_var_value(var_name): + """Returns value for variable in os.environ + + Function throws AssertionError if variable is defined. + Unit-test based python tests require certain input params + to be set in environment, otherwise they can't be run + """ + assert var_name in os.environ.keys(), "Please supply %s in environment" % var_name + return os.environ[var_name] + + +cmdline_credentials = None + +class RpcInterfaceTestCase(TestCase): + """DCE/RPC Test case.""" + + +class ValidNetbiosNameTests(TestCase): + + def test_valid(self): + self.assertTrue(samba.valid_netbios_name("FOO")) + + def test_too_long(self): + self.assertFalse(samba.valid_netbios_name("FOO"*10)) + + def test_invalid_characters(self): + self.assertFalse(samba.valid_netbios_name("*BLA")) + + +class BlackboxProcessError(Exception): + """This is raised when check_output() process returns a non-zero exit status + + Exception instance should contain the exact exit code (S.returncode), + command line (S.cmd), process output (S.stdout) and process error stream + (S.stderr) + """ + + def __init__(self, returncode, cmd, stdout, stderr): + self.returncode = returncode + self.cmd = cmd + self.stdout = stdout + self.stderr = stderr + + def __str__(self): + return "Command '%s'; exit status %d; stdout: '%s'; stderr: '%s'" % (self.cmd, self.returncode, + self.stdout, self.stderr) + +class BlackboxTestCase(TestCase): + """Base test case for blackbox tests.""" + + def _make_cmdline(self, line): + bindir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../../bin")) + parts = line.split(" ") + if os.path.exists(os.path.join(bindir, parts[0])): + parts[0] = os.path.join(bindir, parts[0]) + line = " ".join(parts) + return line + + def check_run(self, line): + line = self._make_cmdline(line) + p = subprocess.Popen(line, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + retcode = p.wait() + if retcode: + raise BlackboxProcessError(retcode, line, p.stdout.read(), p.stderr.read()) + + def check_output(self, line): + line = self._make_cmdline(line) + p = subprocess.Popen(line, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, close_fds=True) + retcode = p.wait() + if retcode: + raise BlackboxProcessError(retcode, line, p.stdout.read(), p.stderr.read()) + return p.stdout.read() + +def connect_samdb(samdb_url, lp=None, session_info=None, credentials=None, + flags=0, ldb_options=None, ldap_only=False): + """Create SamDB instance and connects to samdb_url database. + + :param samdb_url: Url for database to connect to. + :param lp: Optional loadparm object + :param session_info: Optional session information + :param credentials: Optional credentials, defaults to anonymous. + :param flags: Optional LDB flags + :param ldap_only: If set, only remote LDAP connection will be created. + + Added value for tests is that we have a shorthand function + to make proper URL for ldb.connect() while using default + parameters for connection based on test environment + """ + samdb_url = samdb_url.lower() + if not "://" in samdb_url: + if not ldap_only and os.path.isfile(samdb_url): + samdb_url = "tdb://%s" % samdb_url + else: + samdb_url = "ldap://%s" % samdb_url + # use 'paged_search' module when connecting remotely + if samdb_url.startswith("ldap://"): + ldb_options = ["modules:paged_searches"] + elif ldap_only: + raise AssertionError("Trying to connect to %s while remote " + "connection is required" % samdb_url) + + # set defaults for test environment + if lp is None: + lp = env_loadparm() + if session_info is None: + session_info = samba.auth.system_session(lp) + if credentials is None: + credentials = cmdline_credentials + + return SamDB(url=samdb_url, + lp=lp, + session_info=session_info, + credentials=credentials, + flags=flags, + options=ldb_options) + + +def connect_samdb_ex(samdb_url, lp=None, session_info=None, credentials=None, + flags=0, ldb_options=None, ldap_only=False): + """Connects to samdb_url database + + :param samdb_url: Url for database to connect to. + :param lp: Optional loadparm object + :param session_info: Optional session information + :param credentials: Optional credentials, defaults to anonymous. + :param flags: Optional LDB flags + :param ldap_only: If set, only remote LDAP connection will be created. + :return: (sam_db_connection, rootDse_record) tuple + """ + sam_db = connect_samdb(samdb_url, lp, session_info, credentials, + flags, ldb_options, ldap_only) + # fetch RootDse + res = sam_db.search(base="", expression="", scope=ldb.SCOPE_BASE, + attrs=["*"]) + return (sam_db, res[0]) + + +def delete_force(samdb, dn): + try: + samdb.delete(dn) + except ldb.LdbError, (num, _): + assert(num == ldb.ERR_NO_SUCH_OBJECT) diff --git a/python/samba/tests/auth.py b/python/samba/tests/auth.py new file mode 100644 index 00000000000..f71e1a784d2 --- /dev/null +++ b/python/samba/tests/auth.py @@ -0,0 +1,31 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007 +# +# 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/>. +# + +"""Tests for the Auth Python bindings. + +Note that this just tests the bindings work. It does not intend to test +the functionality, that's already done in other tests. +""" + +from samba import auth +import samba.tests + +class AuthTests(samba.tests.TestCase): + + def test_system_session(self): + auth.system_session() + diff --git a/python/samba/tests/blackbox/__init__.py b/python/samba/tests/blackbox/__init__.py new file mode 100644 index 00000000000..361e5cfe5e5 --- /dev/null +++ b/python/samba/tests/blackbox/__init__.py @@ -0,0 +1,17 @@ +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2008 + +# 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/>. +# + +"""Blackbox tests. """ diff --git a/python/samba/tests/blackbox/ndrdump.py b/python/samba/tests/blackbox/ndrdump.py new file mode 100644 index 00000000000..fca9a931533 --- /dev/null +++ b/python/samba/tests/blackbox/ndrdump.py @@ -0,0 +1,49 @@ +# Blackbox tests for ndrdump +# Copyright (C) 2008 Andrew Tridgell <tridge@samba.org> +# Copyright (C) 2008 Andrew Bartlett <abartlet@samba.org> +# Copyright (C) 2010 Jelmer Vernooij <jelmer@samba.org> +# based on test_smbclient.sh + +# 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/>. +# + +"""Blackbox tests for ndrdump.""" + +import os +from samba.tests import BlackboxTestCase + +for p in [ "../../../../../source4/librpc/tests", "../../../../../librpc/tests"]: + data_path_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), p)) + print data_path_dir + if os.path.exists(data_path_dir): + break + + +class NdrDumpTests(BlackboxTestCase): + """Blackbox tests for ndrdump.""" + + def data_path(self, name): + return os.path.join(data_path_dir, name) + + def test_ndrdump_with_in(self): + self.check_run("ndrdump samr samr_CreateUser in %s" % (self.data_path("samr-CreateUser-in.dat"))) + + def test_ndrdump_with_out(self): + self.check_run("ndrdump samr samr_CreateUser out %s" % (self.data_path("samr-CreateUser-out.dat"))) + + def test_ndrdump_context_file(self): + self.check_run("ndrdump --context-file %s samr samr_CreateUser out %s" % (self.data_path("samr-CreateUser-in.dat"), self.data_path("samr-CreateUser-out.dat"))) + + def test_ndrdump_with_validate(self): + self.check_run("ndrdump --validate samr samr_CreateUser in %s" % (self.data_path("samr-CreateUser-in.dat"))) diff --git a/python/samba/tests/blackbox/samba_tool_drs.py b/python/samba/tests/blackbox/samba_tool_drs.py new file mode 100644 index 00000000000..62d7bf123bb --- /dev/null +++ b/python/samba/tests/blackbox/samba_tool_drs.py @@ -0,0 +1,97 @@ +# Blackbox tests for "samba-tool drs" command +# Copyright (C) Kamen Mazdrashki <kamenim@samba.org> 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/>. +# + +"""Blackbox tests for samba-tool drs.""" + +import samba.tests + + +class SambaToolDrsTests(samba.tests.BlackboxTestCase): + """Blackbox test case for samba-tool drs.""" + + def setUp(self): + super(SambaToolDrsTests, self).setUp() + + self.dc1 = samba.tests.env_get_var_value("DC1") + self.dc2 = samba.tests.env_get_var_value("DC2") + + creds = self.get_credentials() + self.cmdline_creds = "-U%s/%s%%%s" % (creds.get_domain(), + creds.get_username(), creds.get_password()) + + def _get_rootDSE(self, dc): + samdb = samba.tests.connect_samdb(dc, lp=self.get_loadparm(), + credentials=self.get_credentials(), + ldap_only=True) + return samdb.search(base="", scope=samba.tests.ldb.SCOPE_BASE)[0] + + def test_samba_tool_bind(self): + """Tests 'samba-tool drs bind' command + Output should be like: + Extensions supported: + <list-of-supported-extensions> + Site GUID: <GUID> + Repl epoch: 0""" + out = self.check_output("samba-tool drs bind %s %s" % (self.dc1, + self.cmdline_creds)) + self.assertTrue("Site GUID:" in out) + self.assertTrue("Repl epoch:" in out) + + def test_samba_tool_kcc(self): + """Tests 'samba-tool drs kcc' command + Output should be like 'Consistency check on <DC> successful.'""" + out = self.check_output("samba-tool drs kcc %s %s" % (self.dc1, + self.cmdline_creds)) + self.assertTrue("Consistency check on" in out) + self.assertTrue("successful" in out) + + def test_samba_tool_showrepl(self): + """Tests 'samba-tool drs showrepl' command + Output should be like: + <site-name>/<domain-name> + DSA Options: <hex-options> + DSA object GUID: <DSA-object-GUID> + DSA invocationId: <DSA-invocationId> + <Inbound-connections-list> + <Outbound-connections-list> + <KCC-objects> + ... + TODO: Perhaps we should check at least for + DSA's objectGUDI and invocationId""" + out = self.check_output("samba-tool drs showrepl %s %s" % (self.dc1, + self.cmdline_creds)) + self.assertTrue("DSA Options:" in out) + self.assertTrue("DSA object GUID:" in out) + self.assertTrue("DSA invocationId:" in out) + + def test_samba_tool_options(self): + """Tests 'samba-tool drs options' command + Output should be like 'Current DSA options: IS_GC <OTHER_FLAGS>'""" + out = self.check_output("samba-tool drs options %s %s" % (self.dc1, + self.cmdline_creds)) + self.assertTrue("Current DSA options:" in out) + + def test_samba_tool_replicate(self): + """Tests 'samba-tool drs replicate' command + Output should be like 'Replicate from <DC-SRC> to <DC-DEST> was successful.'""" + nc_name = self._get_rootDSE(self.dc1)["defaultNamingContext"] + out = self.check_output("samba-tool drs replicate %s %s %s %s" % (self.dc1, + self.dc2, + nc_name, + self.cmdline_creds)) + self.assertTrue("Replicate from" in out) + self.assertTrue("was successful" in out) diff --git a/python/samba/tests/common.py b/python/samba/tests/common.py new file mode 100644 index 00000000000..8794e9dc8b3 --- /dev/null +++ b/python/samba/tests/common.py @@ -0,0 +1,40 @@ +# Unix SMB/CIFS implementation. Tests for common.py routines +# 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/>. +# + +"""Tests for samba.common""" + +import samba, os +import samba.tests +from samba.common import * +from samba.samdb import SamDB + + +class CommonTests(samba.tests.TestCase): + + def test_normalise_int32(self): + self.assertEquals('17', normalise_int32(17)) + self.assertEquals('17', normalise_int32('17')) + self.assertEquals('-123', normalise_int32('-123')) + self.assertEquals('-1294967296', normalise_int32('3000000000')) + + def test_dsdb_Dn(self): + sam = samba.Ldb(url='dntest.ldb') + dn1 = dsdb_Dn(sam, "DC=foo,DC=bar") + dn2 = dsdb_Dn(sam, "B:8:0000000D:<GUID=b3f0ec29-17f4-452a-b002-963e1909d101>;DC=samba,DC=example,DC=com") + self.assertEquals(dn2.binary, "0000000D") + self.assertEquals(13, dn2.get_binary_integer()) + os.unlink('dntest.ldb') diff --git a/python/samba/tests/core.py b/python/samba/tests/core.py new file mode 100644 index 00000000000..8206e68d4fb --- /dev/null +++ b/python/samba/tests/core.py @@ -0,0 +1,63 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2008 +# +# 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/>. +# + +"""Samba Python tests.""" + +import ldb +import os +import samba +from samba.tests import TestCase, TestCaseInTempDir + +class SubstituteVarTestCase(TestCase): + + def test_empty(self): + self.assertEquals("", samba.substitute_var("", {})) + + def test_nothing(self): + self.assertEquals("foo bar", + samba.substitute_var("foo bar", {"bar": "bla"})) + + def test_replace(self): + self.assertEquals("foo bla", + samba.substitute_var("foo ${bar}", {"bar": "bla"})) + + def test_broken(self): + self.assertEquals("foo ${bdkjfhsdkfh sdkfh ", + samba.substitute_var("foo ${bdkjfhsdkfh sdkfh ", {"bar": "bla"})) + + def test_unknown_var(self): + self.assertEquals("foo ${bla} gsff", + samba.substitute_var("foo ${bla} gsff", {"bar": "bla"})) + + def test_check_all_substituted(self): + samba.check_all_substituted("nothing to see here") + self.assertRaises(Exception, samba.check_all_substituted, + "Not subsituted: ${FOOBAR}") + + +class LdbExtensionTests(TestCaseInTempDir): + + def test_searchone(self): + path = self.tempdir + "/searchone.ldb" + l = samba.Ldb(path) + try: + l.add({"dn": "foo=dc", "bar": "bla"}) + self.assertEquals("bla", + l.searchone(basedn=ldb.Dn(l, "foo=dc"), attribute="bar")) + finally: + del l + os.unlink(path) diff --git a/python/samba/tests/credentials.py b/python/samba/tests/credentials.py new file mode 100644 index 00000000000..95ee0fa0deb --- /dev/null +++ b/python/samba/tests/credentials.py @@ -0,0 +1,98 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007 +# +# 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/>. +# + +"""Tests for the Credentials Python bindings. + +Note that this just tests the bindings work. It does not intend to test +the functionality, that's already done in other tests. +""" + +from samba import credentials +import samba.tests + +class CredentialsTests(samba.tests.TestCase): + + def setUp(self): + super(CredentialsTests, self).setUp() + self.creds = credentials.Credentials() + + def test_set_username(self): + self.creds.set_username("somebody") + self.assertEquals("somebody", self.creds.get_username()) + + def test_set_password(self): + self.creds.set_password("S3CreT") + self.assertEquals("S3CreT", self.creds.get_password()) + + def test_set_domain(self): + self.creds.set_domain("ABMAS") + self.assertEquals("ABMAS", self.creds.get_domain()) + + def test_set_realm(self): + self.creds.set_realm("myrealm") + self.assertEquals("MYREALM", self.creds.get_realm()) + + def test_parse_string_anon(self): + self.creds.parse_string("%") + self.assertEquals("", self.creds.get_username()) + self.assertEquals(None, self.creds.get_password()) + + def test_parse_string_user_pw_domain(self): + self.creds.parse_string("dom\\someone%secr") + self.assertEquals("someone", self.creds.get_username()) + self.assertEquals("secr", self.creds.get_password()) + self.assertEquals("DOM", self.creds.get_domain()) + + def test_bind_dn(self): + self.assertEquals(None, self.creds.get_bind_dn()) + self.creds.set_bind_dn("dc=foo,cn=bar") + self.assertEquals("dc=foo,cn=bar", self.creds.get_bind_dn()) + + def test_is_anon(self): + self.creds.set_username("") + self.assertTrue(self.creds.is_anonymous()) + self.creds.set_username("somebody") + self.assertFalse(self.creds.is_anonymous()) + self.creds.set_anonymous() + self.assertTrue(self.creds.is_anonymous()) + + def test_workstation(self): + # FIXME: This is uninitialised, it should be None + #self.assertEquals(None, self.creds.get_workstation()) + self.creds.set_workstation("myworksta") + self.assertEquals("myworksta", self.creds.get_workstation()) + + def test_get_nt_hash(self): + self.creds.set_password("geheim") + self.assertEquals('\xc2\xae\x1f\xe6\xe6H\x84cRE>\x81o*\xeb\x93', + self.creds.get_nt_hash()) + + def test_guess(self): + # Just check the method is there and doesn't raise an exception + self.creds.guess() + + def test_set_cmdline_callbacks(self): + self.creds.set_cmdline_callbacks() + + def test_authentication_requested(self): + self.creds.set_username("") + self.assertFalse(self.creds.authentication_requested()) + self.creds.set_username("somebody") + self.assertTrue(self.creds.authentication_requested()) + + def test_wrong_password(self): + self.assertFalse(self.creds.wrong_password()) diff --git a/python/samba/tests/dcerpc/__init__.py b/python/samba/tests/dcerpc/__init__.py new file mode 100644 index 00000000000..d84cb57a096 --- /dev/null +++ b/python/samba/tests/dcerpc/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# +# Unix SMB/CIFS implementation. +# Copyright © Jelmer Vernooij <jelmer@samba.org> 2008 +# +# 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/>. + +"""Tests for the DCE/RPC Python bindings.""" + diff --git a/python/samba/tests/dcerpc/bare.py b/python/samba/tests/dcerpc/bare.py new file mode 100644 index 00000000000..3efbf9d4cf3 --- /dev/null +++ b/python/samba/tests/dcerpc/bare.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# +# Unix SMB/CIFS implementation. +# Copyright © Jelmer Vernooij <jelmer@samba.org> 2008 +# +# 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/>. +# + +"""Tests for samba.tests.dcerpc.bare.""" + +from samba.dcerpc import ClientConnection +import samba.tests + +class BareTestCase(samba.tests.TestCase): + + def test_bare(self): + # Connect to the echo pipe + x = ClientConnection("ncalrpc:localhost[DEFAULT]", + ("60a15ec5-4de8-11d7-a637-005056a20182", 1), + lp_ctx=samba.tests.env_loadparm()) + self.assertEquals("\x01\x00\x00\x00", x.request(0, chr(0) * 4)) + + def test_alter_context(self): + x = ClientConnection("ncalrpc:localhost[DEFAULT]", + ("12345778-1234-abcd-ef00-0123456789ac", 1), + lp_ctx=samba.tests.env_loadparm()) + y = ClientConnection("ncalrpc:localhost", + ("60a15ec5-4de8-11d7-a637-005056a20182", 1), + basis_connection=x, lp_ctx=samba.tests.env_loadparm()) + x.alter_context(("60a15ec5-4de8-11d7-a637-005056a20182", 1)) + # FIXME: self.assertEquals("\x01\x00\x00\x00", x.request(0, chr(0) * 4)) + + def test_two_connections(self): + x = ClientConnection("ncalrpc:localhost[DEFAULT]", + ("60a15ec5-4de8-11d7-a637-005056a20182", 1), + lp_ctx=samba.tests.env_loadparm()) + y = ClientConnection("ncalrpc:localhost", + ("60a15ec5-4de8-11d7-a637-005056a20182", 1), + basis_connection=x, lp_ctx=samba.tests.env_loadparm()) + self.assertEquals("\x01\x00\x00\x00", y.request(0, chr(0) * 4)) diff --git a/python/samba/tests/dcerpc/dnsserver.py b/python/samba/tests/dcerpc/dnsserver.py new file mode 100644 index 00000000000..59d6eee7618 --- /dev/null +++ b/python/samba/tests/dcerpc/dnsserver.py @@ -0,0 +1,241 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Amitay Isaacs <amitay@gmail.com> 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/>. +# + +"""Tests for samba.dcerpc.dnsserver""" + +from samba.dcerpc import dnsp, dnsserver +from samba.tests import RpcInterfaceTestCase, env_get_var_value +from samba.netcmd.dns import ARecord + +class DnsserverTests(RpcInterfaceTestCase): + + def setUp(self): + super(DnsserverTests, self).setUp() + self.server = env_get_var_value("SERVER_IP") + self.zone = env_get_var_value("REALM").lower() + self.conn = dnsserver.dnsserver("ncacn_ip_tcp:%s" % (self.server), + self.get_loadparm(), + self.get_credentials()) + + def test_operation2(self): + pass + + + def test_query2(self): + typeid, result = self.conn.DnssrvQuery2(dnsserver.DNS_CLIENT_VERSION_W2K, + 0, + self.server, + None, + 'ServerInfo') + self.assertEquals(dnsserver.DNSSRV_TYPEID_SERVER_INFO_W2K, typeid) + + typeid, result = self.conn.DnssrvQuery2(dnsserver.DNS_CLIENT_VERSION_DOTNET, + 0, + self.server, + None, + 'ServerInfo') + self.assertEquals(dnsserver.DNSSRV_TYPEID_SERVER_INFO_DOTNET, typeid) + + typeid, result = self.conn.DnssrvQuery2(dnsserver.DNS_CLIENT_VERSION_LONGHORN, + 0, + self.server, + None, + 'ServerInfo') + self.assertEquals(dnsserver.DNSSRV_TYPEID_SERVER_INFO, typeid) + + def test_operation2(self): + client_version = dnsserver.DNS_CLIENT_VERSION_LONGHORN + rev_zone = '1.168.192.in-addr.arpa' + + zone_create = dnsserver.DNS_RPC_ZONE_CREATE_INFO_LONGHORN() + zone_create.pszZoneName = rev_zone + zone_create.dwZoneType = dnsp.DNS_ZONE_TYPE_PRIMARY + zone_create.fAllowUpdate = dnsp.DNS_ZONE_UPDATE_SECURE + zone_create.fAging = 0 + zone_create.dwDpFlags = dnsserver.DNS_DP_DOMAIN_DEFAULT + + # Create zone + self.conn.DnssrvOperation2(client_version, + 0, + self.server, + None, + 0, + 'ZoneCreate', + dnsserver.DNSSRV_TYPEID_ZONE_CREATE, + zone_create) + + request_filter = (dnsserver.DNS_ZONE_REQUEST_REVERSE | + dnsserver.DNS_ZONE_REQUEST_PRIMARY) + typeid, zones = self.conn.DnssrvComplexOperation2(client_version, + 0, + self.server, + None, + 'EnumZones', + dnsserver.DNSSRV_TYPEID_DWORD, + request_filter) + self.assertEquals(1, zones.dwZoneCount) + + # Delete zone + self.conn.DnssrvOperation2(client_version, + 0, + self.server, + rev_zone, + 0, + 'DeleteZoneFromDs', + dnsserver.DNSSRV_TYPEID_NULL, + None) + + typeid, zones = self.conn.DnssrvComplexOperation2(client_version, + 0, + self.server, + None, + 'EnumZones', + dnsserver.DNSSRV_TYPEID_DWORD, + request_filter) + self.assertEquals(0, zones.dwZoneCount) + + + def test_complexoperation2(self): + client_version = dnsserver.DNS_CLIENT_VERSION_LONGHORN + request_filter = (dnsserver.DNS_ZONE_REQUEST_FORWARD | + dnsserver.DNS_ZONE_REQUEST_PRIMARY) + typeid, zones = self.conn.DnssrvComplexOperation2(client_version, + 0, + self.server, + None, + 'EnumZones', + dnsserver.DNSSRV_TYPEID_DWORD, + request_filter) + self.assertEquals(dnsserver.DNSSRV_TYPEID_ZONE_LIST, typeid) + self.assertEquals(2, zones.dwZoneCount) + + request_filter = (dnsserver.DNS_ZONE_REQUEST_REVERSE | + dnsserver.DNS_ZONE_REQUEST_PRIMARY) + typeid, zones = self.conn.DnssrvComplexOperation2(client_version, + 0, + self.server, + None, + 'EnumZones', + dnsserver.DNSSRV_TYPEID_DWORD, + request_filter) + self.assertEquals(dnsserver.DNSSRV_TYPEID_ZONE_LIST, typeid) + self.assertEquals(0, zones.dwZoneCount) + + + def test_enumrecords2(self): + client_version = dnsserver.DNS_CLIENT_VERSION_LONGHORN + record_type = dnsp.DNS_TYPE_NS + select_flags = (dnsserver.DNS_RPC_VIEW_ROOT_HINT_DATA | + dnsserver.DNS_RPC_VIEW_ADDITIONAL_DATA) + buflen, roothints = self.conn.DnssrvEnumRecords2(client_version, + 0, + self.server, + '..RootHints', + '.', + None, + record_type, + select_flags, + None, + None) + self.assertEquals(14, roothints.count) # 1 NS + 13 A records (a-m) + + + def test_updaterecords2(self): + client_version = dnsserver.DNS_CLIENT_VERSION_LONGHORN + record_type = dnsp.DNS_TYPE_A + select_flags = dnsserver.DNS_RPC_VIEW_AUTHORITY_DATA + + name = 'dummy' + rec = ARecord('1.2.3.4') + rec2 = ARecord('5.6.7.8') + + # Add record + add_rec_buf = dnsserver.DNS_RPC_RECORD_BUF() + add_rec_buf.rec = rec + self.conn.DnssrvUpdateRecord2(client_version, + 0, + self.server, + self.zone, + name, + add_rec_buf, + None) + + buflen, result = self.conn.DnssrvEnumRecords2(client_version, + 0, + self.server, + self.zone, + name, + None, + record_type, + select_flags, + None, + None) + self.assertEquals(1, result.count) + self.assertEquals(1, result.rec[0].wRecordCount) + self.assertEquals(dnsp.DNS_TYPE_A, result.rec[0].records[0].wType) + self.assertEquals('1.2.3.4', result.rec[0].records[0].data) + + # Update record + add_rec_buf = dnsserver.DNS_RPC_RECORD_BUF() + add_rec_buf.rec = rec2 + del_rec_buf = dnsserver.DNS_RPC_RECORD_BUF() + del_rec_buf.rec = rec + self.conn.DnssrvUpdateRecord2(client_version, + 0, + self.server, + self.zone, + name, + add_rec_buf, + del_rec_buf) + + buflen, result = self.conn.DnssrvEnumRecords2(client_version, + 0, + self.server, + self.zone, + name, + None, + record_type, + select_flags, + None, + None) + self.assertEquals(1, result.count) + self.assertEquals(1, result.rec[0].wRecordCount) + self.assertEquals(dnsp.DNS_TYPE_A, result.rec[0].records[0].wType) + self.assertEquals('5.6.7.8', result.rec[0].records[0].data) + + # Delete record + del_rec_buf = dnsserver.DNS_RPC_RECORD_BUF() + del_rec_buf.rec = rec2 + self.conn.DnssrvUpdateRecord2(client_version, + 0, + self.server, + self.zone, + name, + None, + del_rec_buf) + + self.assertRaises(RuntimeError, self.conn.DnssrvEnumRecords2, + client_version, + 0, + self.server, + self.zone, + name, + None, + record_type, + select_flags, + None, + None) diff --git a/python/samba/tests/dcerpc/misc.py b/python/samba/tests/dcerpc/misc.py new file mode 100644 index 00000000000..11e14aadfa7 --- /dev/null +++ b/python/samba/tests/dcerpc/misc.py @@ -0,0 +1,62 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007 +# +# 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/>. +# + +"""Tests for samba.dcerpc.misc.""" + +from samba.dcerpc import misc +import samba.tests + +text1 = "76f53846-a7c2-476a-ae2c-20e2b80d7b34" +text2 = "344edffa-330a-4b39-b96e-2c34da52e8b1" + +class GUIDTests(samba.tests.TestCase): + + def test_str(self): + guid = misc.GUID(text1) + self.assertEquals(text1, str(guid)) + + def test_repr(self): + guid = misc.GUID(text1) + self.assertEquals("GUID('%s')" % text1, repr(guid)) + + def test_compare_different(self): + guid1 = misc.GUID(text1) + guid2 = misc.GUID(text2) + self.assertTrue(cmp(guid1, guid2) > 0) + + def test_compare_same(self): + guid1 = misc.GUID(text1) + guid2 = misc.GUID(text1) + self.assertEquals(0, cmp(guid1, guid2)) + self.assertEquals(guid1, guid2) + + +class PolicyHandleTests(samba.tests.TestCase): + + def test_init(self): + x = misc.policy_handle(text1, 1) + self.assertEquals(1, x.handle_type) + self.assertEquals(text1, str(x.uuid)) + + def test_repr(self): + x = misc.policy_handle(text1, 42) + self.assertEquals("policy_handle(%d, '%s')" % (42, text1), repr(x)) + + def test_str(self): + x = misc.policy_handle(text1, 42) + self.assertEquals("%d, %s" % (42, text1), str(x)) + diff --git a/python/samba/tests/dcerpc/registry.py b/python/samba/tests/dcerpc/registry.py new file mode 100644 index 00000000000..c7bcbfd530a --- /dev/null +++ b/python/samba/tests/dcerpc/registry.py @@ -0,0 +1,51 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2008 +# +# 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/>. +# + +"""Tests for samba.dcerpc.registry.""" + +from samba.dcerpc import winreg +from samba.tests import RpcInterfaceTestCase + + +class WinregTests(RpcInterfaceTestCase): + + def setUp(self): + super(WinregTests, self).setUp() + self.conn = winreg.winreg("ncalrpc:", self.get_loadparm(), + self.get_credentials()) + + def get_hklm(self): + return self.conn.OpenHKLM(None, + winreg.KEY_QUERY_VALUE | winreg.KEY_ENUMERATE_SUB_KEYS) + + def test_hklm(self): + handle = self.conn.OpenHKLM(None, + winreg.KEY_QUERY_VALUE | winreg.KEY_ENUMERATE_SUB_KEYS) + self.conn.CloseKey(handle) + + def test_getversion(self): + handle = self.get_hklm() + version = self.conn.GetVersion(handle) + self.assertEquals(int, version.__class__) + self.conn.CloseKey(handle) + + def test_getkeyinfo(self): + handle = self.conn.OpenHKLM(None, + winreg.KEY_QUERY_VALUE | winreg.KEY_ENUMERATE_SUB_KEYS) + x = self.conn.QueryInfoKey(handle, winreg.String()) + self.assertEquals(9, len(x)) # should return a 9-tuple + self.conn.CloseKey(handle) diff --git a/python/samba/tests/dcerpc/rpc_talloc.py b/python/samba/tests/dcerpc/rpc_talloc.py new file mode 100644 index 00000000000..c091f26c1b9 --- /dev/null +++ b/python/samba/tests/dcerpc/rpc_talloc.py @@ -0,0 +1,84 @@ +# test generated python code from pidl +# Copyright (C) Andrew Tridgell August 2010 +# +# 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/>. +# +# +# to run this test, use one of these: +# +# python -m testtools.run samba.tests.dcerpc.rpc_talloc +# +# or if you have trial installed (from twisted), use +# +# trial samba.tests.dcerpc.rpc_talloc + +"""Tests for the talloc handling in the generated Python DCE/RPC bindings.""" + +import sys + +sys.path.insert(0, "bin/python") + +import samba +import samba.tests +from samba.dcerpc import drsuapi +import talloc + +talloc.enable_null_tracking() + + +class TallocTests(samba.tests.TestCase): + '''test talloc behaviour of pidl generated python code''' + + def check_blocks(self, object, num_expected): + '''check that the number of allocated blocks is correct''' + nblocks = talloc.total_blocks(object) + if object is None: + nblocks -= self.initial_blocks + self.assertEquals(nblocks, num_expected) + + def get_rodc_partial_attribute_set(self): + '''get a list of attributes for RODC replication''' + partial_attribute_set = drsuapi.DsPartialAttributeSet() + + # we expect one block for the object, and one for the structure + self.check_blocks(partial_attribute_set, 2) + + attids = [1, 2, 3] + partial_attribute_set.version = 1 + partial_attribute_set.attids = attids + partial_attribute_set.num_attids = len(attids) + + # we expect one block object, a structure, an ARRAY, and a + # reference to the array + self.check_blocks(partial_attribute_set, 3) + + return partial_attribute_set + + def pas_test(self): + pas = self.get_rodc_partial_attribute_set() + self.check_blocks(pas, 3) + req8 = drsuapi.DsGetNCChangesRequest8() + self.check_blocks(req8, 2) + self.check_blocks(None, 5) + req8.partial_attribute_set = pas + if req8.partial_attribute_set.attids[1] != 2: + raise Exception("Wrong value in attids[2]") + # we now get an additional reference + self.check_blocks(None, 6) + + def test_run(self): + self.initial_blocks = talloc.total_blocks(None) + self.check_blocks(None, 0) + self.pas_test() + self.check_blocks(None, 0) diff --git a/python/samba/tests/dcerpc/rpcecho.py b/python/samba/tests/dcerpc/rpcecho.py new file mode 100644 index 00000000000..099f8f619ce --- /dev/null +++ b/python/samba/tests/dcerpc/rpcecho.py @@ -0,0 +1,71 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2008 +# +# 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/>. +# + +"""Tests for samba.dceprc.rpcecho.""" + +from samba.dcerpc import echo +from samba.ndr import ndr_pack, ndr_unpack +from samba.tests import RpcInterfaceTestCase, TestCase + + +class RpcEchoTests(RpcInterfaceTestCase): + + def setUp(self): + super(RpcEchoTests, self).setUp() + self.conn = echo.rpcecho("ncalrpc:", self.get_loadparm()) + + def test_two_contexts(self): + self.conn2 = echo.rpcecho("ncalrpc:", self.get_loadparm(), basis_connection=self.conn) + self.assertEquals(3, self.conn2.AddOne(2)) + + def test_abstract_syntax(self): + self.assertEquals(("60a15ec5-4de8-11d7-a637-005056a20182", 1), + self.conn.abstract_syntax) + + def test_addone(self): + self.assertEquals(2, self.conn.AddOne(1)) + + def test_echodata(self): + self.assertEquals([1,2,3], self.conn.EchoData([1, 2, 3])) + + def test_call(self): + self.assertEquals(u"foobar", self.conn.TestCall(u"foobar")) + + def test_surrounding(self): + surrounding_struct = echo.Surrounding() + surrounding_struct.x = 4 + surrounding_struct.surrounding = [1,2,3,4] + y = self.conn.TestSurrounding(surrounding_struct) + self.assertEquals(8 * [0], y.surrounding) + + def test_manual_request(self): + self.assertEquals("\x01\x00\x00\x00", self.conn.request(0, chr(0) * 4)) + + def test_server_name(self): + self.assertEquals(None, self.conn.server_name) + + +class NdrEchoTests(TestCase): + + def test_info1_push(self): + x = echo.info1() + x.v = 42 + self.assertEquals("\x2a", ndr_pack(x)) + + def test_info1_pull(self): + x = ndr_unpack(echo.info1, "\x42") + self.assertEquals(x.v, 66) diff --git a/python/samba/tests/dcerpc/sam.py b/python/samba/tests/dcerpc/sam.py new file mode 100644 index 00000000000..0e09323adbe --- /dev/null +++ b/python/samba/tests/dcerpc/sam.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# +# Unix SMB/CIFS implementation. +# Copyright © Jelmer Vernooij <jelmer@samba.org> 2008 +# +# 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/>. +# + +"""Tests for samba.dcerpc.sam.""" + +from samba.dcerpc import samr, security +from samba.tests import RpcInterfaceTestCase + +# FIXME: Pidl should be doing this for us +def toArray((handle, array, num_entries)): + ret = [] + for x in range(num_entries): + ret.append((array.entries[x].idx, array.entries[x].name)) + return ret + + +class SamrTests(RpcInterfaceTestCase): + + def setUp(self): + super(SamrTests, self).setUp() + self.conn = samr.samr("ncalrpc:", self.get_loadparm()) + + def test_connect5(self): + (level, info, handle) = self.conn.Connect5(None, 0, 1, samr.ConnectInfo1()) + + def test_connect2(self): + handle = self.conn.Connect2(None, security.SEC_FLAG_MAXIMUM_ALLOWED) + self.assertTrue(handle is not None) + + def test_EnumDomains(self): + handle = self.conn.Connect2(None, security.SEC_FLAG_MAXIMUM_ALLOWED) + domains = toArray(self.conn.EnumDomains(handle, 0, -1)) + self.conn.Close(handle) + diff --git a/python/samba/tests/dcerpc/srvsvc.py b/python/samba/tests/dcerpc/srvsvc.py new file mode 100644 index 00000000000..3206a27e678 --- /dev/null +++ b/python/samba/tests/dcerpc/srvsvc.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# +# Unix SMB/CIFS implementation. +# Copyright © Dhananjay Sathe <dhanajaysathe@gmail.com> 2011 +# Copyright © Jelmer Vernooij <jelmer@samba.org> 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/>. +# + +"""Tests for samba.dcerpc.srvsvc.""" + +from samba.dcerpc import srvsvc +from samba.tests import RpcInterfaceTestCase + + +class SrvsvcTests(RpcInterfaceTestCase): + + def setUp(self): + super(SrvsvcTests, self).setUp() + self.conn = srvsvc.srvsvc("ncalrpc:", self.get_loadparm()) + self.server_unc = "\\\\." + + def getDummyShareObject(self): + share = srvsvc.NetShareInfo2() + + share.name = u'test' + share.comment = u'test share' + share.type = srvsvc.STYPE_DISKTREE + share.current_users = 0x00000000 + share.max_users = -1 + share.password = None + share.path = u'C:\\tmp' # some random path + share.permissions = 123434566 + return share + + def test_NetShareAdd(self): + self.skip("Dangerous test") + share = self.getDummyShareObject() + self.conn.NetShareAdd(self.server_unc, 2, share, None) + + def test_NetShareSetInfo(self): + self.skip("Dangerous test") + share = self.getDummyShareObject() + parm_error = 0x00000000 + self.conn.NetShareAdd(self.server_unc, 502, share, parm_error) + name = share.name + share.comment = "now sucessfully modified " + parm_error = self.pipe.NetShareSetInfo(self.server_unc, name, + 502, share, parm_error) + + def test_NetShareDel(self): + self.skip("Dangerous test") + share = self.getDummyShareObject() + parm_error = 0x00000000 + self.expectFailure("NetShareAdd doesn't work properly from Python", + self.conn.NetShareAdd, self.server_unc, 502, share, parm_error) + self.conn.NetShareDel(self.server_unc, share.name, 0) diff --git a/python/samba/tests/dcerpc/testrpc.py b/python/samba/tests/dcerpc/testrpc.py new file mode 100644 index 00000000000..e35d6b55446 --- /dev/null +++ b/python/samba/tests/dcerpc/testrpc.py @@ -0,0 +1,141 @@ +# test generated python code from pidl +# Copyright (C) Andrew Tridgell August 2010 +# +# 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 sys + +sys.path.insert(0, "bin/python") + +import samba +import samba.tests +from samba.dcerpc import drsuapi +import talloc + +talloc.enable_null_tracking() + +class RpcTests(object): + '''test type behaviour of pidl generated python RPC code''' + + def check_blocks(self, object, num_expected): + '''check that the number of allocated blocks is correct''' + nblocks = talloc.total_blocks(object) + if object is None: + nblocks -= self.initial_blocks + leaked_blocks = (nblocks - num_expected) + if leaked_blocks != 0: + print "Leaked %d blocks" % leaked_blocks + + def check_type(self, interface, typename, type): + print "Checking type %s" % typename + v = type() + for n in dir(v): + if n[0] == '_': + continue + try: + value = getattr(v, n) + except TypeError, errstr: + if str(errstr) == "unknown union level": + print "ERROR: Unknown union level in %s.%s" % (typename, n) + self.errcount += 1 + continue + print str(errstr)[1:21] + if str(errstr)[0:21] == "Can not convert C Type": + print "ERROR: Unknown C type for %s.%s" % (typename, n) + self.errcount += 1 + continue + else: + print "ERROR: Failed to instantiate %s.%s" % (typename, n) + self.errcount += 1 + continue + except Exception: + print "ERROR: Failed to instantiate %s.%s" % (typename, n) + self.errcount += 1 + continue + + # now try setting the value back + try: + print "Setting %s.%s" % (typename, n) + setattr(v, n, value) + except Exception, e: + if isinstance(e, AttributeError) and str(e).endswith("is read-only"): + # readonly, ignore + continue + else: + print "ERROR: Failed to set %s.%s: %r: %s" % (typename, n, e.__class__, e) + self.errcount += 1 + continue + + # and try a comparison + try: + if value != getattr(v, n): + print "ERROR: Comparison failed for %s.%s: %r != %r" % (typename, n, value, getattr(v, n)) + continue + except Exception, e: + print "ERROR: compare exception for %s.%s: %r: %s" % (typename, n, e.__class__, e) + continue + + def check_interface(self, interface, iname): + errcount = self.errcount + for n in dir(interface): + if n[0] == '_' or n == iname: + # skip the special ones + continue + value = getattr(interface, n) + if isinstance(value, str): + #print "%s=\"%s\"" % (n, value) + pass + elif isinstance(value, int) or isinstance(value, long): + #print "%s=%d" % (n, value) + pass + elif isinstance(value, type): + try: + initial_blocks = talloc.total_blocks(None) + self.check_type(interface, n, value) + self.check_blocks(None, initial_blocks) + except Exception, e: + print "ERROR: Failed to check_type %s.%s: %r: %s" % (iname, n, e.__class__, e) + self.errcount += 1 + elif callable(value): + pass # Method + else: + print "UNKNOWN: %s=%s" % (n, value) + if self.errcount - errcount != 0: + print "Found %d errors in %s" % (self.errcount - errcount, iname) + + def check_all_interfaces(self): + for iname in dir(samba.dcerpc): + if iname[0] == '_': + continue + if iname == 'ClientConnection' or iname == 'base': + continue + print "Checking interface %s" % iname + iface = getattr(samba.dcerpc, iname) + initial_blocks = talloc.total_blocks(None) + self.check_interface(iface, iname) + self.check_blocks(None, initial_blocks) + + def run(self): + self.initial_blocks = talloc.total_blocks(None) + self.errcount = 0 + self.check_all_interfaces() + return self.errcount + +tests = RpcTests() +errcount = tests.run() +if errcount == 0: + sys.exit(0) +else: + print "%d failures" % errcount + sys.exit(1) diff --git a/python/samba/tests/dcerpc/unix.py b/python/samba/tests/dcerpc/unix.py new file mode 100644 index 00000000000..e8ef4da8630 --- /dev/null +++ b/python/samba/tests/dcerpc/unix.py @@ -0,0 +1,49 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2008 +# +# 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/>. +# + +"""Tests for samba.dcerpc.unixinfo.""" + + +from samba.dcerpc import unixinfo +from samba.tests import RpcInterfaceTestCase + +class UnixinfoTests(RpcInterfaceTestCase): + + def setUp(self): + super(UnixinfoTests, self).setUp() + self.conn = unixinfo.unixinfo("ncalrpc:", self.get_loadparm()) + + def test_getpwuid_int(self): + infos = self.conn.GetPWUid(range(512)) + self.assertEquals(512, len(infos)) + self.assertEquals("/bin/false", infos[0].shell) + self.assertTrue(isinstance(infos[0].homedir, unicode)) + + def test_getpwuid(self): + infos = self.conn.GetPWUid(map(long, range(512))) + self.assertEquals(512, len(infos)) + self.assertEquals("/bin/false", infos[0].shell) + self.assertTrue(isinstance(infos[0].homedir, unicode)) + + def test_gidtosid(self): + self.conn.GidToSid(1000L) + + def test_uidtosid(self): + self.conn.UidToSid(1000) + + def test_uidtosid_fail(self): + self.assertRaises(TypeError, self.conn.UidToSid, "100") diff --git a/python/samba/tests/dns.py b/python/samba/tests/dns.py new file mode 100644 index 00000000000..49d699edb78 --- /dev/null +++ b/python/samba/tests/dns.py @@ -0,0 +1,622 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Kai Blin <kai@samba.org> 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 os +import struct +import random +from samba import socket +import samba.ndr as ndr +import samba.dcerpc.dns as dns +from samba.tests import TestCase + +class DNSTest(TestCase): + + def errstr(self, errcode): + "Return a readable error code" + string_codes = [ + "OK", + "FORMERR", + "SERVFAIL", + "NXDOMAIN", + "NOTIMP", + "REFUSED", + "YXDOMAIN", + "YXRRSET", + "NXRRSET", + "NOTAUTH", + "NOTZONE", + ] + + return string_codes[errcode] + + + def assert_dns_rcode_equals(self, packet, rcode): + "Helper function to check return code" + p_errcode = packet.operation & 0x000F + self.assertEquals(p_errcode, rcode, "Expected RCODE %s, got %s" % + (self.errstr(rcode), self.errstr(p_errcode))) + + def assert_dns_opcode_equals(self, packet, opcode): + "Helper function to check opcode" + p_opcode = packet.operation & 0x7800 + self.assertEquals(p_opcode, opcode, "Expected OPCODE %s, got %s" % + (opcode, p_opcode)) + + def make_name_packet(self, opcode, qid=None): + "Helper creating a dns.name_packet" + p = dns.name_packet() + if qid is None: + p.id = random.randint(0x0, 0xffff) + p.operation = opcode + p.questions = [] + return p + + def finish_name_packet(self, packet, questions): + "Helper to finalize a dns.name_packet" + packet.qdcount = len(questions) + packet.questions = questions + + def make_name_question(self, name, qtype, qclass): + "Helper creating a dns.name_question" + q = dns.name_question() + q.name = name + q.question_type = qtype + q.question_class = qclass + return q + + def get_dns_domain(self): + "Helper to get dns domain" + return os.getenv('REALM', 'example.com').lower() + + def dns_transaction_udp(self, packet, host=os.getenv('SERVER_IP')): + "send a DNS query and read the reply" + s = None + try: + send_packet = ndr.ndr_pack(packet) + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0) + s.connect((host, 53)) + s.send(send_packet, 0) + recv_packet = s.recv(2048, 0) + return ndr.ndr_unpack(dns.name_packet, recv_packet) + finally: + if s is not None: + s.close() + + def dns_transaction_tcp(self, packet, host=os.getenv('SERVER_IP')): + "send a DNS query and read the reply" + s = None + try: + send_packet = ndr.ndr_pack(packet) + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) + s.connect((host, 53)) + tcp_packet = struct.pack('!H', len(send_packet)) + tcp_packet += send_packet + s.send(tcp_packet, 0) + recv_packet = s.recv(0xffff + 2, 0) + return ndr.ndr_unpack(dns.name_packet, recv_packet[2:]) + finally: + if s is not None: + s.close() + + +class TestSimpleQueries(DNSTest): + + def test_one_a_query(self): + "create a query packet containing one query record" + p = self.make_name_packet(dns.DNS_OPCODE_QUERY) + questions = [] + + name = "%s.%s" % (os.getenv('SERVER'), self.get_dns_domain()) + q = self.make_name_question(name, dns.DNS_QTYPE_A, dns.DNS_QCLASS_IN) + print "asking for ", q.name + questions.append(q) + + self.finish_name_packet(p, questions) + response = self.dns_transaction_udp(p) + self.assert_dns_rcode_equals(response, dns.DNS_RCODE_OK) + self.assert_dns_opcode_equals(response, dns.DNS_OPCODE_QUERY) + self.assertEquals(response.ancount, 1) + self.assertEquals(response.answers[0].rdata, + os.getenv('SERVER_IP')) + + def test_one_a_query_tcp(self): + "create a query packet containing one query record via TCP" + p = self.make_name_packet(dns.DNS_OPCODE_QUERY) + questions = [] + + name = "%s.%s" % (os.getenv('SERVER'), self.get_dns_domain()) + q = self.make_name_question(name, dns.DNS_QTYPE_A, dns.DNS_QCLASS_IN) + print "asking for ", q.name + questions.append(q) + + self.finish_name_packet(p, questions) + response = self.dns_transaction_tcp(p) + self.assert_dns_rcode_equals(response, dns.DNS_RCODE_OK) + self.assert_dns_opcode_equals(response, dns.DNS_OPCODE_QUERY) + self.assertEquals(response.ancount, 1) + self.assertEquals(response.answers[0].rdata, + os.getenv('SERVER_IP')) + + def test_two_queries(self): + "create a query packet containing two query records" + p = self.make_name_packet(dns.DNS_OPCODE_QUERY) + questions = [] + + name = "%s.%s" % (os.getenv('SERVER'), self.get_dns_domain()) + q = self.make_name_question(name, dns.DNS_QTYPE_A, dns.DNS_QCLASS_IN) + questions.append(q) + + name = "%s.%s" % ('bogusname', self.get_dns_domain()) + q = self.make_name_question(name, dns.DNS_QTYPE_A, dns.DNS_QCLASS_IN) + questions.append(q) + + self.finish_name_packet(p, questions) + response = self.dns_transaction_udp(p) + self.assert_dns_rcode_equals(response, dns.DNS_RCODE_FORMERR) + + def test_qtype_all_query(self): + "create a QTYPE_ALL query" + p = self.make_name_packet(dns.DNS_OPCODE_QUERY) + questions = [] + + name = "%s.%s" % (os.getenv('SERVER'), self.get_dns_domain()) + q = self.make_name_question(name, dns.DNS_QTYPE_ALL, dns.DNS_QCLASS_IN) + print "asking for ", q.name + questions.append(q) + + self.finish_name_packet(p, questions) + response = self.dns_transaction_udp(p) + + num_answers = 1 + dc_ipv6 = os.getenv('SERVER_IPV6') + if dc_ipv6 is not None: + num_answers += 1 + + self.assert_dns_rcode_equals(response, dns.DNS_RCODE_OK) + self.assert_dns_opcode_equals(response, dns.DNS_OPCODE_QUERY) + self.assertEquals(response.ancount, num_answers) + self.assertEquals(response.answers[0].rdata, + os.getenv('SERVER_IP')) + if dc_ipv6 is not None: + self.assertEquals(response.answers[1].rdata, dc_ipv6) + + def test_qclass_none_query(self): + "create a QCLASS_NONE query" + p = self.make_name_packet(dns.DNS_OPCODE_QUERY) + questions = [] + + name = "%s.%s" % (os.getenv('SERVER'), self.get_dns_domain()) + q = self.make_name_question(name, dns.DNS_QTYPE_ALL, dns.DNS_QCLASS_NONE) + questions.append(q) + + self.finish_name_packet(p, questions) + response = self.dns_transaction_udp(p) + self.assert_dns_rcode_equals(response, dns.DNS_RCODE_NOTIMP) + +# Only returns an authority section entry in BIND and Win DNS +# FIXME: Enable one Samba implements this feature +# def test_soa_hostname_query(self): +# "create a SOA query for a hostname" +# p = self.make_name_packet(dns.DNS_OPCODE_QUERY) +# questions = [] +# +# name = "%s.%s" % (os.getenv('SERVER'), self.get_dns_domain()) +# q = self.make_name_question(name, dns.DNS_QTYPE_SOA, dns.DNS_QCLASS_IN) +# questions.append(q) +# +# self.finish_name_packet(p, questions) +# response = self.dns_transaction_udp(p) +# self.assert_dns_rcode_equals(response, dns.DNS_RCODE_OK) +# self.assert_dns_opcode_equals(response, dns.DNS_OPCODE_QUERY) +# # We don't get SOA records for single hosts +# self.assertEquals(response.ancount, 0) + + def test_soa_domain_query(self): + "create a SOA query for a domain" + p = self.make_name_packet(dns.DNS_OPCODE_QUERY) + questions = [] + + name = self.get_dns_domain() + q = self.make_name_question(name, dns.DNS_QTYPE_SOA, dns.DNS_QCLASS_IN) + questions.append(q) + + self.finish_name_packet(p, questions) + response = self.dns_transaction_udp(p) + self.assert_dns_rcode_equals(response, dns.DNS_RCODE_OK) + self.assert_dns_opcode_equals(response, dns.DNS_OPCODE_QUERY) + self.assertEquals(response.ancount, 1) + + +class TestDNSUpdates(DNSTest): + + def test_two_updates(self): + "create two update requests" + p = self.make_name_packet(dns.DNS_OPCODE_UPDATE) + updates = [] + + name = "%s.%s" % (os.getenv('SERVER'), self.get_dns_domain()) + u = self.make_name_question(name, dns.DNS_QTYPE_A, dns.DNS_QCLASS_IN) + updates.append(u) + + name = self.get_dns_domain() + u = self.make_name_question(name, dns.DNS_QTYPE_A, dns.DNS_QCLASS_IN) + updates.append(u) + + self.finish_name_packet(p, updates) + response = self.dns_transaction_udp(p) + self.assert_dns_rcode_equals(response, dns.DNS_RCODE_FORMERR) + + def test_update_wrong_qclass(self): + "create update with DNS_QCLASS_NONE" + p = self.make_name_packet(dns.DNS_OPCODE_UPDATE) + updates = [] + + name = self.get_dns_domain() + u = self.make_name_question(name, dns.DNS_QTYPE_A, dns.DNS_QCLASS_NONE) + updates.append(u) + + self.finish_name_packet(p, updates) + response = self.dns_transaction_udp(p) + self.assert_dns_rcode_equals(response, dns.DNS_RCODE_NOTIMP) + + def test_update_prereq_with_non_null_ttl(self): + "test update with a non-null TTL" + p = self.make_name_packet(dns.DNS_OPCODE_UPDATE) + updates = [] + + name = self.get_dns_domain() + + u = self.make_name_question(name, dns.DNS_QTYPE_SOA, dns.DNS_QCLASS_IN) + updates.append(u) + self.finish_name_packet(p, updates) + + prereqs = [] + r = dns.res_rec() + r.name = "%s.%s" % (os.getenv('SERVER'), self.get_dns_domain()) + r.rr_type = dns.DNS_QTYPE_TXT + r.rr_class = dns.DNS_QCLASS_NONE + r.ttl = 1 + r.length = 0 + prereqs.append(r) + + p.ancount = len(prereqs) + p.answers = prereqs + + response = self.dns_transaction_udp(p) + self.assert_dns_rcode_equals(response, dns.DNS_RCODE_FORMERR) + +# I'd love to test this one, but it segfaults. :) +# def test_update_prereq_with_non_null_length(self): +# "test update with a non-null length" +# p = self.make_name_packet(dns.DNS_OPCODE_UPDATE) +# updates = [] +# +# name = self.get_dns_domain() +# +# u = self.make_name_question(name, dns.DNS_QTYPE_SOA, dns.DNS_QCLASS_IN) +# updates.append(u) +# self.finish_name_packet(p, updates) +# +# prereqs = [] +# r = dns.res_rec() +# r.name = "%s.%s" % (os.getenv('SERVER'), self.get_dns_domain()) +# r.rr_type = dns.DNS_QTYPE_TXT +# r.rr_class = dns.DNS_QCLASS_ANY +# r.ttl = 0 +# r.length = 1 +# prereqs.append(r) +# +# p.ancount = len(prereqs) +# p.answers = prereqs +# +# response = self.dns_transaction_udp(p) +# self.assert_dns_rcode_equals(response, dns.DNS_RCODE_FORMERR) + + def test_update_prereq_nonexisting_name(self): + "test update with a nonexisting name" + p = self.make_name_packet(dns.DNS_OPCODE_UPDATE) + updates = [] + + name = self.get_dns_domain() + + u = self.make_name_question(name, dns.DNS_QTYPE_SOA, dns.DNS_QCLASS_IN) + updates.append(u) + self.finish_name_packet(p, updates) + + prereqs = [] + r = dns.res_rec() + r.name = "idontexist.%s" % self.get_dns_domain() + r.rr_type = dns.DNS_QTYPE_TXT + r.rr_class = dns.DNS_QCLASS_ANY + r.ttl = 0 + r.length = 0 + prereqs.append(r) + + p.ancount = len(prereqs) + p.answers = prereqs + + response = self.dns_transaction_udp(p) + self.assert_dns_rcode_equals(response, dns.DNS_RCODE_NXRRSET) + + def test_update_add_txt_record(self): + "test adding records works" + p = self.make_name_packet(dns.DNS_OPCODE_UPDATE) + updates = [] + + name = self.get_dns_domain() + + u = self.make_name_question(name, dns.DNS_QTYPE_SOA, dns.DNS_QCLASS_IN) + updates.append(u) + self.finish_name_packet(p, updates) + + updates = [] + r = dns.res_rec() + r.name = "textrec.%s" % self.get_dns_domain() + r.rr_type = dns.DNS_QTYPE_TXT + r.rr_class = dns.DNS_QCLASS_IN + r.ttl = 900 + r.length = 0xffff + r.rdata = dns.txt_record() + r.rdata.txt = '"This is a test"' + updates.append(r) + p.nscount = len(updates) + p.nsrecs = updates + + response = self.dns_transaction_udp(p) + self.assert_dns_rcode_equals(response, dns.DNS_RCODE_OK) + + p = self.make_name_packet(dns.DNS_OPCODE_QUERY) + questions = [] + + name = "textrec.%s" % self.get_dns_domain() + q = self.make_name_question(name, dns.DNS_QTYPE_TXT, dns.DNS_QCLASS_IN) + questions.append(q) + + self.finish_name_packet(p, questions) + response = self.dns_transaction_udp(p) + self.assert_dns_rcode_equals(response, dns.DNS_RCODE_OK) + self.assertEquals(response.ancount, 1) + self.assertEquals(response.answers[0].rdata.txt, '"This is a test"') + + def test_update_add_two_txt_records(self): + "test adding two txt records works" + p = self.make_name_packet(dns.DNS_OPCODE_UPDATE) + updates = [] + + name = self.get_dns_domain() + + u = self.make_name_question(name, dns.DNS_QTYPE_SOA, dns.DNS_QCLASS_IN) + updates.append(u) + self.finish_name_packet(p, updates) + + updates = [] + r = dns.res_rec() + r.name = "textrec2.%s" % self.get_dns_domain() + r.rr_type = dns.DNS_QTYPE_TXT + r.rr_class = dns.DNS_QCLASS_IN + r.ttl = 900 + r.length = 0xffff + r.rdata = dns.txt_record() + r.rdata.txt = '"This is a test" "and this is a test, too"' + updates.append(r) + p.nscount = len(updates) + p.nsrecs = updates + + response = self.dns_transaction_udp(p) + self.assert_dns_rcode_equals(response, dns.DNS_RCODE_OK) + + p = self.make_name_packet(dns.DNS_OPCODE_QUERY) + questions = [] + + name = "textrec2.%s" % self.get_dns_domain() + q = self.make_name_question(name, dns.DNS_QTYPE_TXT, dns.DNS_QCLASS_IN) + questions.append(q) + + self.finish_name_packet(p, questions) + response = self.dns_transaction_udp(p) + self.assert_dns_rcode_equals(response, dns.DNS_RCODE_OK) + self.assertEquals(response.ancount, 1) + self.assertEquals(response.answers[0].rdata.txt, '"This is a test" "and this is a test, too"') + + def test_delete_record(self): + "Test if deleting records works" + + NAME = "deleterec.%s" % self.get_dns_domain() + + # First, create a record to make sure we have a record to delete. + p = self.make_name_packet(dns.DNS_OPCODE_UPDATE) + updates = [] + + name = self.get_dns_domain() + + u = self.make_name_question(name, dns.DNS_QTYPE_SOA, dns.DNS_QCLASS_IN) + updates.append(u) + self.finish_name_packet(p, updates) + + updates = [] + r = dns.res_rec() + r.name = NAME + r.rr_type = dns.DNS_QTYPE_TXT + r.rr_class = dns.DNS_QCLASS_IN + r.ttl = 900 + r.length = 0xffff + r.rdata = dns.txt_record() + r.rdata.txt = '"This is a test"' + updates.append(r) + p.nscount = len(updates) + p.nsrecs = updates + + response = self.dns_transaction_udp(p) + self.assert_dns_rcode_equals(response, dns.DNS_RCODE_OK) + + # Now check the record is around + p = self.make_name_packet(dns.DNS_OPCODE_QUERY) + questions = [] + q = self.make_name_question(NAME, dns.DNS_QTYPE_TXT, dns.DNS_QCLASS_IN) + questions.append(q) + + self.finish_name_packet(p, questions) + response = self.dns_transaction_udp(p) + self.assert_dns_rcode_equals(response, dns.DNS_RCODE_OK) + + # Now delete the record + p = self.make_name_packet(dns.DNS_OPCODE_UPDATE) + updates = [] + + name = self.get_dns_domain() + + u = self.make_name_question(name, dns.DNS_QTYPE_SOA, dns.DNS_QCLASS_IN) + updates.append(u) + self.finish_name_packet(p, updates) + + updates = [] + r = dns.res_rec() + r.name = NAME + r.rr_type = dns.DNS_QTYPE_TXT + r.rr_class = dns.DNS_QCLASS_NONE + r.ttl = 0 + r.length = 0xffff + r.rdata = dns.txt_record() + r.rdata.txt = '"This is a test"' + updates.append(r) + p.nscount = len(updates) + p.nsrecs = updates + + response = self.dns_transaction_udp(p) + self.assert_dns_rcode_equals(response, dns.DNS_RCODE_OK) + + # And finally check it's gone + p = self.make_name_packet(dns.DNS_OPCODE_QUERY) + questions = [] + + q = self.make_name_question(NAME, dns.DNS_QTYPE_TXT, dns.DNS_QCLASS_IN) + questions.append(q) + + self.finish_name_packet(p, questions) + response = self.dns_transaction_udp(p) + self.assert_dns_rcode_equals(response, dns.DNS_RCODE_NXDOMAIN) + + +class TestComplexQueries(DNSTest): + + def setUp(self): + super(TestComplexQueries, self).setUp() + p = self.make_name_packet(dns.DNS_OPCODE_UPDATE) + updates = [] + + name = self.get_dns_domain() + + u = self.make_name_question(name, dns.DNS_QTYPE_SOA, dns.DNS_QCLASS_IN) + updates.append(u) + self.finish_name_packet(p, updates) + + updates = [] + r = dns.res_rec() + r.name = "cname_test.%s" % self.get_dns_domain() + r.rr_type = dns.DNS_QTYPE_CNAME + r.rr_class = dns.DNS_QCLASS_IN + r.ttl = 900 + r.length = 0xffff + r.rdata = "%s.%s" % (os.getenv('SERVER'), self.get_dns_domain()) + updates.append(r) + p.nscount = len(updates) + p.nsrecs = updates + + response = self.dns_transaction_udp(p) + self.assert_dns_rcode_equals(response, dns.DNS_RCODE_OK) + + def tearDown(self): + super(TestComplexQueries, self).tearDown() + p = self.make_name_packet(dns.DNS_OPCODE_UPDATE) + updates = [] + + name = self.get_dns_domain() + + u = self.make_name_question(name, dns.DNS_QTYPE_SOA, dns.DNS_QCLASS_IN) + updates.append(u) + self.finish_name_packet(p, updates) + + updates = [] + r = dns.res_rec() + r.name = "cname_test.%s" % self.get_dns_domain() + r.rr_type = dns.DNS_QTYPE_CNAME + r.rr_class = dns.DNS_QCLASS_NONE + r.ttl = 0 + r.length = 0xffff + r.rdata = "%s.%s" % (os.getenv('SERVER'), self.get_dns_domain()) + updates.append(r) + p.nscount = len(updates) + p.nsrecs = updates + + response = self.dns_transaction_udp(p) + self.assert_dns_rcode_equals(response, dns.DNS_RCODE_OK) + + def test_one_a_query(self): + "create a query packet containing one query record" + p = self.make_name_packet(dns.DNS_OPCODE_QUERY) + questions = [] + + name = "cname_test.%s" % self.get_dns_domain() + q = self.make_name_question(name, dns.DNS_QTYPE_A, dns.DNS_QCLASS_IN) + print "asking for ", q.name + questions.append(q) + + self.finish_name_packet(p, questions) + response = self.dns_transaction_udp(p) + self.assert_dns_rcode_equals(response, dns.DNS_RCODE_OK) + self.assert_dns_opcode_equals(response, dns.DNS_OPCODE_QUERY) + self.assertEquals(response.ancount, 2) + self.assertEquals(response.answers[0].rr_type, dns.DNS_QTYPE_CNAME) + self.assertEquals(response.answers[0].rdata, "%s.%s" % + (os.getenv('SERVER'), self.get_dns_domain())) + self.assertEquals(response.answers[1].rr_type, dns.DNS_QTYPE_A) + self.assertEquals(response.answers[1].rdata, + os.getenv('SERVER_IP')) + +class TestInvalidQueries(DNSTest): + + def test_one_a_query(self): + "send 0 bytes follows by create a query packet containing one query record" + + s = None + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0) + s.connect((os.getenv('SERVER_IP'), 53)) + s.send("", 0) + finally: + if s is not None: + s.close() + + p = self.make_name_packet(dns.DNS_OPCODE_QUERY) + questions = [] + + name = "%s.%s" % (os.getenv('SERVER'), self.get_dns_domain()) + q = self.make_name_question(name, dns.DNS_QTYPE_A, dns.DNS_QCLASS_IN) + print "asking for ", q.name + questions.append(q) + + self.finish_name_packet(p, questions) + response = self.dns_transaction_udp(p) + self.assert_dns_rcode_equals(response, dns.DNS_RCODE_OK) + self.assert_dns_opcode_equals(response, dns.DNS_OPCODE_QUERY) + self.assertEquals(response.ancount, 1) + self.assertEquals(response.answers[0].rdata, + os.getenv('SERVER_IP')) + +if __name__ == "__main__": + import unittest + unittest.main() diff --git a/python/samba/tests/docs.py b/python/samba/tests/docs.py new file mode 100644 index 00000000000..c1b371680d7 --- /dev/null +++ b/python/samba/tests/docs.py @@ -0,0 +1,127 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2012 +# +# Tests for documentation. +# +# 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/>. +# + +"""Tests for presence of documentation.""" + +import samba +import samba.tests +from samba.tests import TestSkipped + +import errno +import os +import re +import subprocess + + +class TestCase(samba.tests.TestCase): + + def _format_message(self, parameters, message): + parameters = list(parameters) + parameters.sort() + return message + '\n\n %s' % ('\n '.join(parameters)) + + +class NoXsltProc(Exception): + + def __init__(self): + Exception.__init__(self, "'xsltproc' is not installed") + + +def get_documented_parameters(sourcedir): + path = os.path.join(sourcedir, "bin", "default", "docs-xml", "smbdotconf") + if not os.path.exists(os.path.join(path, "parameters.all.xml")): + raise Exception("Unable to find parameters.all.xml") + try: + p = subprocess.Popen( + ["xsltproc", "--xinclude", "--param", "smb.context", "ALL", os.path.join(sourcedir, "docs-xml", "smbdotconf", "generate-context.xsl"), "parameters.all.xml"], + stderr=subprocess.STDOUT, stdout=subprocess.PIPE, + cwd=path) + except OSError, e: + if e.errno == errno.ENOENT: + raise NoXsltProc() + raise + out, err = p.communicate() + assert p.returncode == 0, "returncode was %r" % p.returncode + for l in out.splitlines(): + m = re.match('<samba:parameter .*?name="([^"]*?)"', l) + if "removed=\"1\"" in l: + continue + if m: + name = m.group(1) + yield name + m = re.match('.*<synonym>(.*)</synonym>.*', l) + if m: + name = m.group(1) + yield name + + +def get_implementation_parameters(sourcedir): + # Reading entries from source code + f = open(os.path.join(sourcedir, "lib/param/param_table.c"), "r") + try: + # burn through the preceding lines + while True: + l = f.readline() + if l.startswith("static struct parm_struct parm_table"): + break + + for l in f.readlines(): + if re.match("^\s*\}\;\s*$", l): + break + # pull in the param names only + if re.match(".*P_SEPARATOR.*", l): + continue + m = re.match("\s*\.label\s*=\s*\"(.*)\".*", l) + if not m: + continue + + name = m.group(1) + yield name + finally: + f.close() + + +class SmbDotConfTests(TestCase): + + def test_unknown(self): + topdir = samba.source_tree_topdir() + try: + documented = set(get_documented_parameters(topdir)) + except NoXsltProc: + raise TestSkipped("'xsltproc' is missing, unable to load parameters") + parameters = set(get_implementation_parameters(topdir)) + # Filter out parametric options, since we can't find them in the parm + # table + documented = set([p for p in documented if not ":" in p]) + unknown = documented.difference(parameters) + if len(unknown) > 0: + self.fail(self._format_message(unknown, + "Parameters that are documented but not in the implementation:")) + + def test_undocumented(self): + topdir = samba.source_tree_topdir() + try: + documented = set(get_documented_parameters(topdir)) + except NoXsltProc: + raise TestSkipped("'xsltproc' is missing, unable to load parameters") + parameters = set(get_implementation_parameters(topdir)) + undocumented = parameters.difference(documented) + if len(undocumented) > 0: + self.fail(self._format_message(undocumented, + "Parameters that are in the implementation but undocumented:")) diff --git a/python/samba/tests/dsdb.py b/python/samba/tests/dsdb.py new file mode 100644 index 00000000000..3aef1d2fa48 --- /dev/null +++ b/python/samba/tests/dsdb.py @@ -0,0 +1,130 @@ +# Unix SMB/CIFS implementation. Tests for dsdb +# Copyright (C) Matthieu Patou <mat@matws.net> 2010 +# +# 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/>. +# + +"""Tests for samba.dsdb.""" + +from samba.credentials import Credentials +from samba.samdb import SamDB +from samba.auth import system_session +from samba.tests import TestCase +from samba.ndr import ndr_unpack, ndr_pack +from samba.dcerpc import drsblobs +import ldb +import os +import samba + + +class DsdbTests(TestCase): + + def setUp(self): + super(DsdbTests, self).setUp() + self.lp = samba.param.LoadParm() + self.lp.load(os.path.join(os.path.join(self.baseprovpath(), "etc"), "smb.conf")) + self.creds = Credentials() + self.creds.guess(self.lp) + self.session = system_session() + self.samdb = SamDB(os.path.join(self.baseprovpath(), "private", "sam.ldb"), + session_info=self.session, credentials=self.creds,lp=self.lp) + + def baseprovpath(self): + return os.path.join(os.environ['SELFTEST_PREFIX'], "dc") + + def test_get_oid_from_attrid(self): + oid = self.samdb.get_oid_from_attid(591614) + self.assertEquals(oid, "1.2.840.113556.1.4.1790") + + def test_error_replpropertymetadata(self): + res = self.samdb.search(expression="cn=Administrator", + scope=ldb.SCOPE_SUBTREE, + attrs=["replPropertyMetaData"]) + repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob, + str(res[0]["replPropertyMetaData"])) + ctr = repl.ctr + for o in ctr.array: + # Search for Description + if o.attid == 13: + old_version = o.version + o.version = o.version + 1 + replBlob = ndr_pack(repl) + msg = ldb.Message() + msg.dn = res[0].dn + msg["replPropertyMetaData"] = ldb.MessageElement(replBlob, ldb.FLAG_MOD_REPLACE, "replPropertyMetaData") + self.assertRaises(ldb.LdbError, self.samdb.modify, msg, ["local_oid:1.3.6.1.4.1.7165.4.3.14:0"]) + + def test_twoatt_replpropertymetadata(self): + res = self.samdb.search(expression="cn=Administrator", + scope=ldb.SCOPE_SUBTREE, + attrs=["replPropertyMetaData", "uSNChanged"]) + repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob, + str(res[0]["replPropertyMetaData"])) + ctr = repl.ctr + for o in ctr.array: + # Search for Description + if o.attid == 13: + old_version = o.version + o.version = o.version + 1 + o.local_usn = long(str(res[0]["uSNChanged"])) + 1 + replBlob = ndr_pack(repl) + msg = ldb.Message() + msg.dn = res[0].dn + msg["replPropertyMetaData"] = ldb.MessageElement(replBlob, ldb.FLAG_MOD_REPLACE, "replPropertyMetaData") + msg["description"] = ldb.MessageElement("new val", ldb.FLAG_MOD_REPLACE, "description") + self.assertRaises(ldb.LdbError, self.samdb.modify, msg, ["local_oid:1.3.6.1.4.1.7165.4.3.14:0"]) + + def test_set_replpropertymetadata(self): + res = self.samdb.search(expression="cn=Administrator", + scope=ldb.SCOPE_SUBTREE, + attrs=["replPropertyMetaData", "uSNChanged"]) + repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob, + str(res[0]["replPropertyMetaData"])) + ctr = repl.ctr + for o in ctr.array: + # Search for Description + if o.attid == 13: + old_version = o.version + o.version = o.version + 1 + o.local_usn = long(str(res[0]["uSNChanged"])) + 1 + o.originating_usn = long(str(res[0]["uSNChanged"])) + 1 + replBlob = ndr_pack(repl) + msg = ldb.Message() + msg.dn = res[0].dn + msg["replPropertyMetaData"] = ldb.MessageElement(replBlob, ldb.FLAG_MOD_REPLACE, "replPropertyMetaData") + self.samdb.modify(msg, ["local_oid:1.3.6.1.4.1.7165.4.3.14:0"]) + + def test_ok_get_attribute_from_attid(self): + self.assertEquals(self.samdb.get_attribute_from_attid(13), "description") + + def test_ko_get_attribute_from_attid(self): + self.assertEquals(self.samdb.get_attribute_from_attid(11979), None) + + def test_get_attribute_replmetadata_version(self): + res = self.samdb.search(expression="cn=Administrator", + scope=ldb.SCOPE_SUBTREE, + attrs=["dn"]) + self.assertEquals(len(res), 1) + dn = str(res[0].dn) + self.assertEqual(self.samdb.get_attribute_replmetadata_version(dn, "unicodePwd"), 1) + + def test_set_attribute_replmetadata_version(self): + res = self.samdb.search(expression="cn=Administrator", + scope=ldb.SCOPE_SUBTREE, + attrs=["dn"]) + self.assertEquals(len(res), 1) + dn = str(res[0].dn) + version = self.samdb.get_attribute_replmetadata_version(dn, "description") + self.samdb.set_attribute_replmetadata_version(dn, "description", version + 2) + self.assertEqual(self.samdb.get_attribute_replmetadata_version(dn, "description"), version + 2) diff --git a/python/samba/tests/gensec.py b/python/samba/tests/gensec.py new file mode 100644 index 00000000000..e270c418ea6 --- /dev/null +++ b/python/samba/tests/gensec.py @@ -0,0 +1,146 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2009 +# +# 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/>. +# + +"""Tests for GENSEC. + +Note that this just tests the bindings work. It does not intend to test +the functionality, that's already done in other tests. +""" + +from samba.credentials import Credentials +from samba import gensec, auth +import samba.tests + +class GensecTests(samba.tests.TestCase): + + def setUp(self): + super(GensecTests, self).setUp() + self.settings = {} + self.settings["lp_ctx"] = self.lp_ctx = samba.tests.env_loadparm() + self.settings["target_hostname"] = self.lp_ctx.get("netbios name") + """This is just for the API tests""" + self.gensec = gensec.Security.start_client(self.settings) + + def test_start_mech_by_unknown_name(self): + self.assertRaises(RuntimeError, self.gensec.start_mech_by_name, "foo") + + def test_start_mech_by_name(self): + self.gensec.start_mech_by_name("spnego") + + def test_info_uninitialized(self): + self.assertRaises(RuntimeError, self.gensec.session_info) + + def test_update(self): + """Test GENSEC by doing an exchange with ourselves using GSSAPI against a KDC""" + + """Start up a client and server GENSEC instance to test things with""" + + self.gensec_client = gensec.Security.start_client(self.settings) + self.gensec_client.set_credentials(self.get_credentials()) + self.gensec_client.want_feature(gensec.FEATURE_SEAL) + self.gensec_client.start_mech_by_sasl_name("GSSAPI") + + self.gensec_server = gensec.Security.start_server(settings=self.settings, + auth_context=auth.AuthContext(lp_ctx=self.lp_ctx)) + creds = Credentials() + creds.guess(self.lp_ctx) + creds.set_machine_account(self.lp_ctx) + self.gensec_server.set_credentials(creds) + + self.gensec_server.want_feature(gensec.FEATURE_SEAL) + self.gensec_server.start_mech_by_sasl_name("GSSAPI") + + client_finished = False + server_finished = False + server_to_client = "" + + """Run the actual call loop""" + while not client_finished and not server_finished: + if not client_finished: + print "running client gensec_update" + (client_finished, client_to_server) = self.gensec_client.update(server_to_client) + if not server_finished: + print "running server gensec_update" + (server_finished, server_to_client) = self.gensec_server.update(client_to_server) + session_info = self.gensec_server.session_info() + + test_string = "Hello Server" + test_wrapped = self.gensec_client.wrap(test_string) + test_unwrapped = self.gensec_server.unwrap(test_wrapped) + self.assertEqual(test_string, test_unwrapped) + test_string = "Hello Client" + test_wrapped = self.gensec_server.wrap(test_string) + test_unwrapped = self.gensec_client.unwrap(test_wrapped) + self.assertEqual(test_string, test_unwrapped) + + client_session_key = self.gensec_client.session_key() + server_session_key = self.gensec_server.session_key() + self.assertEqual(client_session_key, server_session_key) + + def test_max_update_size(self): + """Test GENSEC by doing an exchange with ourselves using GSSAPI against a KDC""" + + """Start up a client and server GENSEC instance to test things with""" + + self.gensec_client = gensec.Security.start_client(self.settings) + self.gensec_client.set_credentials(self.get_credentials()) + self.gensec_client.want_feature(gensec.FEATURE_SIGN) + self.gensec_client.set_max_update_size(5) + self.gensec_client.start_mech_by_name("spnego") + + self.gensec_server = gensec.Security.start_server(settings=self.settings, + auth_context=auth.AuthContext(lp_ctx=self.lp_ctx)) + creds = Credentials() + creds.guess(self.lp_ctx) + creds.set_machine_account(self.lp_ctx) + self.gensec_server.set_credentials(creds) + self.gensec_server.want_feature(gensec.FEATURE_SIGN) + self.gensec_server.set_max_update_size(5) + self.gensec_server.start_mech_by_name("spnego") + + client_finished = False + server_finished = False + server_to_client = "" + + """Run the actual call loop""" + i = 0 + while not client_finished or not server_finished: + i += 1 + if not client_finished: + print "running client gensec_update: %d: %r" % (len(server_to_client), server_to_client) + (client_finished, client_to_server) = self.gensec_client.update(server_to_client) + if not server_finished: + print "running server gensec_update: %d: %r" % (len(client_to_server), client_to_server) + (server_finished, server_to_client) = self.gensec_server.update(client_to_server) + + """Here we expect a lot more than the typical 1 or 2 roundtrips""" + self.assertTrue(i > 10) + + session_info = self.gensec_server.session_info() + + test_string = "Hello Server" + test_wrapped = self.gensec_client.wrap(test_string) + test_unwrapped = self.gensec_server.unwrap(test_wrapped) + self.assertEqual(test_string, test_unwrapped) + test_string = "Hello Client" + test_wrapped = self.gensec_server.wrap(test_string) + test_unwrapped = self.gensec_client.unwrap(test_wrapped) + self.assertEqual(test_string, test_unwrapped) + + client_session_key = self.gensec_client.session_key() + server_session_key = self.gensec_server.session_key() + self.assertEqual(client_session_key, server_session_key) diff --git a/python/samba/tests/getopt.py b/python/samba/tests/getopt.py new file mode 100644 index 00000000000..14ee0a7428b --- /dev/null +++ b/python/samba/tests/getopt.py @@ -0,0 +1,55 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 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/>. +# + +"""Tests for option parsing. + +""" + +import optparse +from samba.getopt import ( + AUTO_USE_KERBEROS, + DONT_USE_KERBEROS, + MUST_USE_KERBEROS, + parse_kerberos_arg, + ) +import samba.tests + +class KerberosOptionTests(samba.tests.TestCase): + + def test_parse_true(self): + self.assertEquals( + MUST_USE_KERBEROS, parse_kerberos_arg("yes", "--kerberos")) + self.assertEquals( + MUST_USE_KERBEROS, parse_kerberos_arg("true", "--kerberos")) + self.assertEquals( + MUST_USE_KERBEROS, parse_kerberos_arg("1", "--kerberos")) + + def test_parse_false(self): + self.assertEquals( + DONT_USE_KERBEROS, parse_kerberos_arg("no", "--kerberos")) + self.assertEquals( + DONT_USE_KERBEROS, parse_kerberos_arg("false", "--kerberos")) + self.assertEquals( + DONT_USE_KERBEROS, parse_kerberos_arg("0", "--kerberos")) + + def test_parse_auto(self): + self.assertEquals( + AUTO_USE_KERBEROS, parse_kerberos_arg("auto", "--kerberos")) + + def test_parse_invalid(self): + self.assertRaises(optparse.OptionValueError, + parse_kerberos_arg, "blah?", "--kerberos") diff --git a/python/samba/tests/hostconfig.py b/python/samba/tests/hostconfig.py new file mode 100644 index 00000000000..526dc0fe4e3 --- /dev/null +++ b/python/samba/tests/hostconfig.py @@ -0,0 +1,74 @@ +# Unix SMB/CIFS implementation. Tests for shares +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2009 +# +# 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/>. +# + +"""Tests for samba.hostconfig.""" + +from samba.hostconfig import SharesContainer +from samba.tests import TestCase + + +class MockService(object): + + def __init__(self, data): + self.data = data + + def __getitem__(self, name): + return self.data[name] + + +class MockLoadParm(object): + + def __init__(self, data): + self.data = data + + def __getitem__(self, name): + return MockService(self.data[name]) + + def __len__(self): + return len(self.data) + + def services(self): + return self.data.keys() + + +class ShareTests(TestCase): + + def _get_shares(self, conf): + return SharesContainer(MockLoadParm(conf)) + + def test_len_no_global(self): + shares = self._get_shares({}) + self.assertEquals(0, len(shares)) + + def test_iter(self): + self.assertEquals([], list(self._get_shares({}))) + self.assertEquals([], list(self._get_shares({"global":{}}))) + self.assertEquals( + ["bla"], + list(self._get_shares({"global":{}, "bla":{}}))) + + def test_len(self): + shares = self._get_shares({"global": {}}) + self.assertEquals(0, len(shares)) + + def test_getitem_nonexistent(self): + shares = self._get_shares({"global": {}}) + self.assertRaises(KeyError, shares.__getitem__, "bla") + + def test_getitem_global(self): + shares = self._get_shares({"global": {}}) + self.assertRaises(KeyError, shares.__getitem__, "global") diff --git a/python/samba/tests/libsmb_samba_internal.py b/python/samba/tests/libsmb_samba_internal.py new file mode 100644 index 00000000000..fe9f197a2c2 --- /dev/null +++ b/python/samba/tests/libsmb_samba_internal.py @@ -0,0 +1,78 @@ +# Unix SMB/CIFS implementation. +# Copyright Volker Lendecke <vl@samba.org> 2012 +# +# 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/>. +# + +"""Tests for samba.samba3.libsmb_samba_internal.""" + +from samba.samba3 import libsmb_samba_internal +from samba.dcerpc import security +from samba.samba3 import param as s3param +from samba import credentials +import samba.tests +import threading +import sys +import os + +class LibsmbTestCase(samba.tests.TestCase): + + class OpenClose(threading.Thread): + + def __init__(self, conn, filename, num_ops): + threading.Thread.__init__(self) + self.conn = conn + self.filename = filename + self.num_ops = num_ops + self.exc = False + + def run(self): + c = self.conn + try: + for i in range(self.num_ops): + f = c.create(self.filename, CreateDisposition=3, + DesiredAccess=security.SEC_STD_DELETE) + c.delete_on_close(f, True) + c.close(f) + except Exception: + self.exc = sys.exc_info() + + def test_OpenClose(self): + + lp = s3param.get_context() + lp.load(os.getenv("SMB_CONF_PATH")) + + creds = credentials.Credentials() + creds.set_username(os.getenv("USERNAME")) + creds.set_password(os.getenv("PASSWORD")) + + c = libsmb_samba_internal.Conn(os.getenv("SERVER_IP"), "tmp", creds) + + mythreads = [] + + for i in range(3): + t = LibsmbTestCase.OpenClose(c, "test" + str(i), 10) + mythreads.append(t) + + for t in mythreads: + t.start() + + for t in mythreads: + t.join() + if t.exc: + raise t.exc[0](t.exc[1]) + +if __name__ == "__main__": + import unittest + unittest.main() diff --git a/python/samba/tests/messaging.py b/python/samba/tests/messaging.py new file mode 100644 index 00000000000..f0cd368195f --- /dev/null +++ b/python/samba/tests/messaging.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# +# Unix SMB/CIFS implementation. +# Copyright © Jelmer Vernooij <jelmer@samba.org> 2008 +# +# 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/>. +# + +"""Tests for samba.messaging.""" + +from samba.messaging import Messaging +from samba.tests import TestCase +from samba.dcerpc.server_id import server_id + +class MessagingTests(TestCase): + + def get_context(self, *args, **kwargs): + return Messaging(*args, **kwargs) + + def test_register(self): + x = self.get_context() + def callback(): + pass + msg_type = x.register(callback) + x.deregister(callback, msg_type) + + def test_all_servers(self): + x = self.get_context() + self.assertTrue(isinstance(x.irpc_all_servers(), list)) + + def test_by_name(self): + x = self.get_context() + for name in x.irpc_all_servers(): + self.assertTrue(isinstance(x.irpc_servers_byname(name.name), list)) + + def test_assign_server_id(self): + x = self.get_context() + self.assertTrue(isinstance(x.server_id, server_id)) + + def test_ping_speed(self): + server_ctx = self.get_context((0, 1)) + def ping_callback(src, data): + server_ctx.send(src, data) + def exit_callback(): + print "received exit" + msg_ping = server_ctx.register(ping_callback) + msg_exit = server_ctx.register(exit_callback) + + def pong_callback(): + print "received pong" + client_ctx = self.get_context((0, 2)) + msg_pong = client_ctx.register(pong_callback) + + client_ctx.send((0, 1), msg_ping, "testing") + client_ctx.send((0, 1), msg_ping, "") + diff --git a/python/samba/tests/netcmd.py b/python/samba/tests/netcmd.py new file mode 100644 index 00000000000..2cbac4e8bff --- /dev/null +++ b/python/samba/tests/netcmd.py @@ -0,0 +1,90 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2009-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/>. +# + +"""Tests for samba.netcmd.""" + +from cStringIO import StringIO +from samba.netcmd import Command +from samba.netcmd.testparm import cmd_testparm +from samba.netcmd.main import cmd_sambatool +import samba.tests + +class NetCmdTestCase(samba.tests.TestCase): + + def run_netcmd(self, cmd_klass, args, retcode=0): + cmd = cmd_klass(outf=StringIO(), errf=StringIO()) + try: + retval = cmd._run(cmd_klass.__name__, *args) + except Exception, e: + cmd.show_command_error(e) + retval = 1 + self.assertEquals(retcode, retval) + return cmd.outf.getvalue(), cmd.errf.getvalue() + + def iter_all_subcommands(self): + todo = [] + todo.extend(cmd_sambatool.subcommands.items()) + while todo: + (path, cmd) = todo.pop() + yield path, cmd + subcmds = getattr(cmd, "subcommands", {}) + todo.extend([(path + " " + k, v) for (k, v) in + subcmds.iteritems()]) + + +class TestParmTests(NetCmdTestCase): + + def test_no_client_ip(self): + out, err = self.run_netcmd(cmd_testparm, ["--client-name=foo"], + retcode=-1) + self.assertEquals("", out) + self.assertEquals( + "ERROR: Both a DNS name and an IP address are " + "required for the host access check\n", err) + + +class CommandTests(NetCmdTestCase): + + def test_description(self): + class cmd_foo(Command): + """Mydescription""" + self.assertEquals("Mydescription", cmd_foo().short_description) + + def test_name(self): + class cmd_foo(Command): + pass + self.assertEquals("foo", cmd_foo().name) + + def test_synopsis_everywhere(self): + missing = [] + for path, cmd in self.iter_all_subcommands(): + if cmd.synopsis is None: + missing.append(path) + if missing: + self.fail("The following commands do not have a synopsis set: %r" % + missing) + + def test_short_description_everywhere(self): + missing = [] + for path, cmd in self.iter_all_subcommands(): + if cmd.short_description is None: + missing.append(path) + if not missing: + return + self.fail( + "The following commands do not have a short description set: %r" % + missing) diff --git a/python/samba/tests/ntacls.py b/python/samba/tests/ntacls.py new file mode 100644 index 00000000000..aa9ef6852ba --- /dev/null +++ b/python/samba/tests/ntacls.py @@ -0,0 +1,83 @@ +# Unix SMB/CIFS implementation. Tests for ntacls manipulation +# Copyright (C) Matthieu Patou <mat@matws.net> 2009-2010 +# Copyright (C) Andrew Bartlett 2012 +# +# 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/>. +# + +"""Tests for samba.ntacls.""" + +from samba.ntacls import setntacl, getntacl, XattrBackendError +from samba.dcerpc import xattr, security +from samba.param import LoadParm +from samba.tests import TestCaseInTempDir, TestSkipped +import random +import os + +class NtaclsTests(TestCaseInTempDir): + + def test_setntacl(self): + lp = LoadParm() + acl = "O:S-1-5-21-2212615479-2695158682-2101375467-512G:S-1-5-21-2212615479-2695158682-2101375467-513D:(A;OICI;0x001f01ff;;;S-1-5-21-2212615479-2695158682-2101375467-512)" + open(self.tempf, 'w').write("empty") + lp.set("posix:eadb",os.path.join(self.tempdir,"eadbtest.tdb")) + setntacl(lp, self.tempf, acl, "S-1-5-21-2212615479-2695158682-2101375467") + os.unlink(os.path.join(self.tempdir,"eadbtest.tdb")) + + def test_setntacl_getntacl(self): + lp = LoadParm() + acl = "O:S-1-5-21-2212615479-2695158682-2101375467-512G:S-1-5-21-2212615479-2695158682-2101375467-513D:(A;OICI;0x001f01ff;;;S-1-5-21-2212615479-2695158682-2101375467-512)" + open(self.tempf, 'w').write("empty") + lp.set("posix:eadb",os.path.join(self.tempdir,"eadbtest.tdb")) + setntacl(lp,self.tempf,acl,"S-1-5-21-2212615479-2695158682-2101375467") + facl = getntacl(lp,self.tempf) + anysid = security.dom_sid(security.SID_NT_SELF) + self.assertEquals(facl.as_sddl(anysid),acl) + os.unlink(os.path.join(self.tempdir,"eadbtest.tdb")) + + def test_setntacl_getntacl_param(self): + lp = LoadParm() + acl = "O:S-1-5-21-2212615479-2695158682-2101375467-512G:S-1-5-21-2212615479-2695158682-2101375467-513D:(A;OICI;0x001f01ff;;;S-1-5-21-2212615479-2695158682-2101375467-512)" + open(self.tempf, 'w').write("empty") + setntacl(lp,self.tempf,acl,"S-1-5-21-2212615479-2695158682-2101375467","tdb",os.path.join(self.tempdir,"eadbtest.tdb")) + facl=getntacl(lp,self.tempf,"tdb",os.path.join(self.tempdir,"eadbtest.tdb")) + domsid=security.dom_sid(security.SID_NT_SELF) + self.assertEquals(facl.as_sddl(domsid),acl) + os.unlink(os.path.join(self.tempdir,"eadbtest.tdb")) + + def test_setntacl_invalidbackend(self): + lp = LoadParm() + acl = "O:S-1-5-21-2212615479-2695158682-2101375467-512G:S-1-5-21-2212615479-2695158682-2101375467-513D:(A;OICI;0x001f01ff;;;S-1-5-21-2212615479-2695158682-2101375467-512)" + open(self.tempf, 'w').write("empty") + self.assertRaises(XattrBackendError, setntacl, lp, self.tempf, acl, "S-1-5-21-2212615479-2695158682-2101375467","ttdb", os.path.join(self.tempdir,"eadbtest.tdb")) + + def test_setntacl_forcenative(self): + if os.getuid() == 0: + raise TestSkipped("Running test as root, test skipped") + lp = LoadParm() + acl = "O:S-1-5-21-2212615479-2695158682-2101375467-512G:S-1-5-21-2212615479-2695158682-2101375467-513D:(A;OICI;0x001f01ff;;;S-1-5-21-2212615479-2695158682-2101375467-512)" + open(self.tempf, 'w').write("empty") + lp.set("posix:eadb", os.path.join(self.tempdir,"eadbtest.tdb")) + self.assertRaises(Exception, setntacl, lp, self.tempf ,acl, + "S-1-5-21-2212615479-2695158682-2101375467","native") + + + def setUp(self): + super(NtaclsTests, self).setUp() + self.tempf = os.path.join(self.tempdir, "test") + open(self.tempf, 'w').write("empty") + + def tearDown(self): + os.unlink(self.tempf) + super(NtaclsTests, self).tearDown() diff --git a/python/samba/tests/param.py b/python/samba/tests/param.py new file mode 100644 index 00000000000..f539eba140d --- /dev/null +++ b/python/samba/tests/param.py @@ -0,0 +1,57 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007 +# +# 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/>. +# + +"""Tests for samba.param.""" + +from samba import param +import samba.tests + +class LoadParmTestCase(samba.tests.TestCase): + + def test_init(self): + file = param.LoadParm() + self.assertTrue(file is not None) + + def test_length(self): + file = param.LoadParm() + self.assertEquals(0, len(file)) + + def test_set_workgroup(self): + file = param.LoadParm() + file.set("workgroup", "bla") + self.assertEquals("BLA", file.get("workgroup")) + + def test_is_mydomain(self): + file = param.LoadParm() + file.set("workgroup", "bla") + self.assertTrue(file.is_mydomain("BLA")) + self.assertFalse(file.is_mydomain("FOOBAR")) + + def test_is_myname(self): + file = param.LoadParm() + file.set("netbios name", "bla") + self.assertTrue(file.is_myname("BLA")) + self.assertFalse(file.is_myname("FOOBAR")) + + def test_load_default(self): + file = param.LoadParm() + file.load_default() + + def test_section_nonexistent(self): + samba_lp = param.LoadParm() + samba_lp.load_default() + self.assertRaises(KeyError, samba_lp.__getitem__, "nonexistent") diff --git a/python/samba/tests/policy.py b/python/samba/tests/policy.py new file mode 100644 index 00000000000..b2457451528 --- /dev/null +++ b/python/samba/tests/policy.py @@ -0,0 +1,34 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2010 +# +# 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/>. +# + +"""Tests for the libpolicy Python bindings. + +""" + +from samba.tests import TestCase +from samba import policy + + +class PolicyTests(TestCase): + + def test_get_gpo_flags(self): + self.assertEquals(["GPO_FLAG_USER_DISABLE"], + policy.get_gpo_flags(policy.GPO_FLAG_USER_DISABLE)) + + def test_get_gplink_options(self): + self.assertEquals(["GPLINK_OPT_DISABLE"], + policy.get_gplink_options(policy.GPLINK_OPT_DISABLE)) diff --git a/python/samba/tests/posixacl.py b/python/samba/tests/posixacl.py new file mode 100644 index 00000000000..7cd22ebccd1 --- /dev/null +++ b/python/samba/tests/posixacl.py @@ -0,0 +1,732 @@ +# Unix SMB/CIFS implementation. Tests for NT and posix ACL manipulation +# Copyright (C) Matthieu Patou <mat@matws.net> 2009-2010 +# Copyright (C) Andrew Bartlett 2012 +# +# 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/>. +# + +"""Tests for the Samba3 NT -> posix ACL layer""" + +from samba.ntacls import setntacl, getntacl, checkset_backend +from samba.dcerpc import xattr, security, smb_acl, idmap +from samba.param import LoadParm +from samba.tests import TestCaseInTempDir +from samba import provision +import random +import os +from samba.samba3 import smbd, passdb +from samba.samba3 import param as s3param + +# To print a posix ACL use: +# for entry in posix_acl.acl: +# print "a_type: %d" % entry.a_type +# print "a_perm: %o" % entry.a_perm +# print "uid: %d" % entry.uid +# print "gid: %d" % entry.gid + +class PosixAclMappingTests(TestCaseInTempDir): + + def test_setntacl(self): + acl = "O:S-1-5-21-2212615479-2695158682-2101375467-512G:S-1-5-21-2212615479-2695158682-2101375467-513D:(A;OICI;0x001f01ff;;;S-1-5-21-2212615479-2695158682-2101375467-512)" + setntacl(self.lp, self.tempf, acl, "S-1-5-21-2212615479-2695158682-2101375467", use_ntvfs=False) + + def test_setntacl_smbd_getntacl(self): + acl = "O:S-1-5-21-2212615479-2695158682-2101375467-512G:S-1-5-21-2212615479-2695158682-2101375467-513D:(A;OICI;0x001f01ff;;;S-1-5-21-2212615479-2695158682-2101375467-512)" + setntacl(self.lp, self.tempf, acl, "S-1-5-21-2212615479-2695158682-2101375467", use_ntvfs=True) + facl = getntacl(self.lp, self.tempf, direct_db_access=True) + anysid = security.dom_sid(security.SID_NT_SELF) + self.assertEquals(facl.as_sddl(anysid),acl) + + def test_setntacl_smbd_setposixacl_getntacl(self): + acl = "O:S-1-5-21-2212615479-2695158682-2101375467-512G:S-1-5-21-2212615479-2695158682-2101375467-513D:(A;OICI;0x001f01ff;;;S-1-5-21-2212615479-2695158682-2101375467-512)" + setntacl(self.lp, self.tempf, acl, "S-1-5-21-2212615479-2695158682-2101375467", use_ntvfs=True) + + # This will invalidate the ACL, as we have a hook! + smbd.set_simple_acl(self.tempf, 0640) + + # However, this only asks the xattr + try: + facl = getntacl(self.lp, self.tempf, direct_db_access=True) + self.assertTrue(False) + except TypeError: + pass + + def test_setntacl_invalidate_getntacl(self): + acl = "O:S-1-5-21-2212615479-2695158682-2101375467-512G:S-1-5-21-2212615479-2695158682-2101375467-513D:(A;OICI;0x001f01ff;;;S-1-5-21-2212615479-2695158682-2101375467-512)" + setntacl(self.lp, self.tempf, acl, "S-1-5-21-2212615479-2695158682-2101375467", use_ntvfs=True) + + # This should invalidate the ACL, as we include the posix ACL in the hash + (backend_obj, dbname) = checkset_backend(self.lp, None, None) + backend_obj.wrap_setxattr(dbname, + self.tempf, "system.fake_access_acl", "") + + #however, as this is direct DB access, we do not notice it + facl = getntacl(self.lp, self.tempf, direct_db_access=True) + anysid = security.dom_sid(security.SID_NT_SELF) + self.assertEquals(acl, facl.as_sddl(anysid)) + + def test_setntacl_invalidate_getntacl_smbd(self): + acl = "O:S-1-5-21-2212615479-2695158682-2101375467-512G:S-1-5-21-2212615479-2695158682-2101375467-513D:(A;OICI;0x001f01ff;;;S-1-5-21-2212615479-2695158682-2101375467-512)" + setntacl(self.lp, self.tempf, acl, "S-1-5-21-2212615479-2695158682-2101375467", use_ntvfs=False) + + # This should invalidate the ACL, as we include the posix ACL in the hash + (backend_obj, dbname) = checkset_backend(self.lp, None, None) + backend_obj.wrap_setxattr(dbname, + self.tempf, "system.fake_access_acl", "") + + #the hash would break, and we return an ACL based only on the mode, except we set the ACL using the 'ntvfs' mode that doesn't include a hash + facl = getntacl(self.lp, self.tempf) + anysid = security.dom_sid(security.SID_NT_SELF) + self.assertEquals(acl, facl.as_sddl(anysid)) + + def test_setntacl_smbd_invalidate_getntacl_smbd(self): + acl = "O:S-1-5-21-2212615479-2695158682-2101375467-512G:S-1-5-21-2212615479-2695158682-2101375467-513D:(A;OICI;0x001f01ff;;;S-1-5-21-2212615479-2695158682-2101375467-512)" + simple_acl_from_posix = "O:S-1-5-21-2212615479-2695158682-2101375467-512G:S-1-5-21-2212615479-2695158682-2101375467-513D:(A;;0x001f01ff;;;S-1-5-21-2212615479-2695158682-2101375467-512)(A;;0x001200a9;;;S-1-5-21-2212615479-2695158682-2101375467-513)(A;;;;;WD)" + os.chmod(self.tempf, 0750) + setntacl(self.lp, self.tempf, acl, "S-1-5-21-2212615479-2695158682-2101375467", use_ntvfs=False) + + # This should invalidate the ACL, as we include the posix ACL in the hash + (backend_obj, dbname) = checkset_backend(self.lp, None, None) + backend_obj.wrap_setxattr(dbname, + self.tempf, "system.fake_access_acl", "") + + #the hash will break, and we return an ACL based only on the mode + facl = getntacl(self.lp, self.tempf, direct_db_access=False) + anysid = security.dom_sid(security.SID_NT_SELF) + self.assertEquals(simple_acl_from_posix, facl.as_sddl(anysid)) + + def test_setntacl_smbd_dont_invalidate_getntacl_smbd(self): + # set an ACL on a tempfile + acl = "O:S-1-5-21-2212615479-2695158682-2101375467-512G:S-1-5-21-2212615479-2695158682-2101375467-513D:(A;OICI;0x001f01ff;;;S-1-5-21-2212615479-2695158682-2101375467-512)" + os.chmod(self.tempf, 0750) + setntacl(self.lp, self.tempf, acl, "S-1-5-21-2212615479-2695158682-2101375467", use_ntvfs=False) + + # now influence the POSIX ACL->SD mapping it returns something else than + # what was set previously + # this should not invalidate the hash and the complete ACL should still + # be returned + self.lp.set("profile acls", "yes") + # we should still get back the ACL (and not one mapped from POSIX ACL) + facl = getntacl(self.lp, self.tempf, direct_db_access=False) + self.lp.set("profile acls", "no") + anysid = security.dom_sid(security.SID_NT_SELF) + self.assertEquals(acl, facl.as_sddl(anysid)) + + def test_setntacl_getntacl_smbd(self): + acl = "O:S-1-5-21-2212615479-2695158682-2101375467-512G:S-1-5-21-2212615479-2695158682-2101375467-513D:(A;OICI;0x001f01ff;;;S-1-5-21-2212615479-2695158682-2101375467-512)" + setntacl(self.lp, self.tempf, acl, "S-1-5-21-2212615479-2695158682-2101375467", use_ntvfs=True) + facl = getntacl(self.lp, self.tempf, direct_db_access=False) + anysid = security.dom_sid(security.SID_NT_SELF) + self.assertEquals(facl.as_sddl(anysid),acl) + + def test_setntacl_smbd_getntacl_smbd(self): + acl = "O:S-1-5-21-2212615479-2695158682-2101375467-512G:S-1-5-21-2212615479-2695158682-2101375467-513D:(A;OICI;0x001f01ff;;;S-1-5-21-2212615479-2695158682-2101375467-512)" + setntacl(self.lp, self.tempf, acl, "S-1-5-21-2212615479-2695158682-2101375467", use_ntvfs=False) + facl = getntacl(self.lp, self.tempf, direct_db_access=False) + anysid = security.dom_sid(security.SID_NT_SELF) + self.assertEquals(facl.as_sddl(anysid),acl) + + def test_setntacl_smbd_setposixacl_getntacl_smbd(self): + acl = "O:S-1-5-21-2212615479-2695158682-2101375467-512G:S-1-5-21-2212615479-2695158682-2101375467-513D:(A;OICI;0x001f01ff;;;S-1-5-21-2212615479-2695158682-2101375467-512)" + simple_acl_from_posix = "O:S-1-5-21-2212615479-2695158682-2101375467-512G:S-1-5-21-2212615479-2695158682-2101375467-513D:(A;;0x001f019f;;;S-1-5-21-2212615479-2695158682-2101375467-512)(A;;0x00120089;;;S-1-5-21-2212615479-2695158682-2101375467-513)(A;;;;;WD)" + setntacl(self.lp, self.tempf, acl, "S-1-5-21-2212615479-2695158682-2101375467", use_ntvfs=False) + # This invalidates the hash of the NT acl just set because there is a hook in the posix ACL set code + smbd.set_simple_acl(self.tempf, 0640) + facl = getntacl(self.lp, self.tempf, direct_db_access=False) + anysid = security.dom_sid(security.SID_NT_SELF) + self.assertEquals(simple_acl_from_posix, facl.as_sddl(anysid)) + + def test_setntacl_smbd_setposixacl_group_getntacl_smbd(self): + acl = "O:S-1-5-21-2212615479-2695158682-2101375467-512G:S-1-5-21-2212615479-2695158682-2101375467-513D:(A;OICI;0x001f01ff;;;S-1-5-21-2212615479-2695158682-2101375467-512)" + BA_sid = security.dom_sid(security.SID_BUILTIN_ADMINISTRATORS) + simple_acl_from_posix = "O:S-1-5-21-2212615479-2695158682-2101375467-512G:S-1-5-21-2212615479-2695158682-2101375467-513D:(A;;0x001f019f;;;S-1-5-21-2212615479-2695158682-2101375467-512)(A;;0x00120089;;;BA)(A;;0x00120089;;;S-1-5-21-2212615479-2695158682-2101375467-513)(A;;;;;WD)" + setntacl(self.lp, self.tempf, acl, "S-1-5-21-2212615479-2695158682-2101375467", use_ntvfs=False) + # This invalidates the hash of the NT acl just set because there is a hook in the posix ACL set code + s4_passdb = passdb.PDB(self.lp.get("passdb backend")) + (BA_gid,BA_type) = s4_passdb.sid_to_id(BA_sid) + smbd.set_simple_acl(self.tempf, 0640, BA_gid) + + # This should re-calculate an ACL based on the posix details + facl = getntacl(self.lp,self.tempf, direct_db_access=False) + anysid = security.dom_sid(security.SID_NT_SELF) + self.assertEquals(simple_acl_from_posix, facl.as_sddl(anysid)) + + def test_setntacl_smbd_getntacl_smbd_gpo(self): + acl = "O:DAG:DUD:P(A;OICI;0x001f01ff;;;DA)(A;OICI;0x001f01ff;;;EA)(A;OICIIO;0x001f01ff;;;CO)(A;OICI;0x001f01ff;;;DA)(A;OICI;0x001f01ff;;;SY)(A;OICI;0x001200a9;;;AU)(A;OICI;0x001200a9;;;ED)S:AI(OU;CIIDSA;WP;f30e3bbe-9ff0-11d1-b603-0000f80367c1;bf967aa5-0de6-11d0-a285-00aa003049e2;WD)(OU;CIIDSA;WP;f30e3bbf-9ff0-11d1-b603-0000f80367c1;bf967aa5-0de6-11d0-a285-00aa003049e2;WD)" + setntacl(self.lp, self.tempf, acl, "S-1-5-21-2212615479-2695158682-2101375467", use_ntvfs=False) + facl = getntacl(self.lp, self.tempf, direct_db_access=False) + domsid = security.dom_sid("S-1-5-21-2212615479-2695158682-2101375467") + self.assertEquals(facl.as_sddl(domsid),acl) + + def test_setntacl_getposixacl(self): + acl = "O:S-1-5-21-2212615479-2695158682-2101375467-512G:S-1-5-21-2212615479-2695158682-2101375467-513D:(A;OICI;0x001f01ff;;;S-1-5-21-2212615479-2695158682-2101375467-512)" + setntacl(self.lp, self.tempf, acl, "S-1-5-21-2212615479-2695158682-2101375467", use_ntvfs=False) + facl = getntacl(self.lp, self.tempf) + anysid = security.dom_sid(security.SID_NT_SELF) + self.assertEquals(facl.as_sddl(anysid),acl) + posix_acl = smbd.get_sys_acl(self.tempf, smb_acl.SMB_ACL_TYPE_ACCESS) + + def test_setposixacl_getposixacl(self): + smbd.set_simple_acl(self.tempf, 0640) + posix_acl = smbd.get_sys_acl(self.tempf, smb_acl.SMB_ACL_TYPE_ACCESS) + self.assertEquals(posix_acl.count, 4) + + self.assertEquals(posix_acl.acl[0].a_type, smb_acl.SMB_ACL_USER_OBJ) + self.assertEquals(posix_acl.acl[0].a_perm, 6) + + self.assertEquals(posix_acl.acl[1].a_type, smb_acl.SMB_ACL_GROUP_OBJ) + self.assertEquals(posix_acl.acl[1].a_perm, 4) + + self.assertEquals(posix_acl.acl[2].a_type, smb_acl.SMB_ACL_OTHER) + self.assertEquals(posix_acl.acl[2].a_perm, 0) + + self.assertEquals(posix_acl.acl[3].a_type, smb_acl.SMB_ACL_MASK) + self.assertEquals(posix_acl.acl[3].a_perm, 6) + + def test_setposixacl_getntacl(self): + acl = "" + smbd.set_simple_acl(self.tempf, 0750) + try: + facl = getntacl(self.lp, self.tempf) + self.assertTrue(False) + except TypeError: + # We don't expect the xattr to be filled in in this case + pass + + def test_setposixacl_getntacl_smbd(self): + s4_passdb = passdb.PDB(self.lp.get("passdb backend")) + group_SID = s4_passdb.gid_to_sid(os.stat(self.tempf).st_gid) + user_SID = s4_passdb.uid_to_sid(os.stat(self.tempf).st_uid) + smbd.set_simple_acl(self.tempf, 0640) + facl = getntacl(self.lp, self.tempf, direct_db_access=False) + acl = "O:%sG:%sD:(A;;0x001f019f;;;%s)(A;;0x00120089;;;%s)(A;;;;;WD)" % (user_SID, group_SID, user_SID, group_SID) + anysid = security.dom_sid(security.SID_NT_SELF) + self.assertEquals(acl, facl.as_sddl(anysid)) + + def test_setposixacl_dir_getntacl_smbd(self): + s4_passdb = passdb.PDB(self.lp.get("passdb backend")) + user_SID = s4_passdb.uid_to_sid(os.stat(self.tempdir).st_uid) + BA_sid = security.dom_sid(security.SID_BUILTIN_ADMINISTRATORS) + s4_passdb = passdb.PDB(self.lp.get("passdb backend")) + (BA_id,BA_type) = s4_passdb.sid_to_id(BA_sid) + self.assertEquals(BA_type, idmap.ID_TYPE_BOTH) + SO_sid = security.dom_sid(security.SID_BUILTIN_SERVER_OPERATORS) + (SO_id,SO_type) = s4_passdb.sid_to_id(SO_sid) + self.assertEquals(SO_type, idmap.ID_TYPE_BOTH) + smbd.chown(self.tempdir, BA_id, SO_id) + smbd.set_simple_acl(self.tempdir, 0750) + facl = getntacl(self.lp, self.tempdir, direct_db_access=False) + acl = "O:BAG:SOD:(A;;0x001f01ff;;;BA)(A;;0x001200a9;;;SO)(A;;;;;WD)(A;OICIIO;0x001f01ff;;;CO)(A;OICIIO;0x001f01ff;;;CG)(A;OICIIO;0x001f01ff;;;WD)" + + anysid = security.dom_sid(security.SID_NT_SELF) + self.assertEquals(acl, facl.as_sddl(anysid)) + + def test_setposixacl_group_getntacl_smbd(self): + BA_sid = security.dom_sid(security.SID_BUILTIN_ADMINISTRATORS) + s4_passdb = passdb.PDB(self.lp.get("passdb backend")) + (BA_gid,BA_type) = s4_passdb.sid_to_id(BA_sid) + group_SID = s4_passdb.gid_to_sid(os.stat(self.tempf).st_gid) + user_SID = s4_passdb.uid_to_sid(os.stat(self.tempf).st_uid) + self.assertEquals(BA_type, idmap.ID_TYPE_BOTH) + smbd.set_simple_acl(self.tempf, 0640, BA_gid) + facl = getntacl(self.lp, self.tempf, direct_db_access=False) + domsid = passdb.get_global_sam_sid() + acl = "O:%sG:%sD:(A;;0x001f019f;;;%s)(A;;0x00120089;;;BA)(A;;0x00120089;;;%s)(A;;;;;WD)" % (user_SID, group_SID, user_SID, group_SID) + anysid = security.dom_sid(security.SID_NT_SELF) + self.assertEquals(acl, facl.as_sddl(anysid)) + + def test_setposixacl_getposixacl(self): + smbd.set_simple_acl(self.tempf, 0640) + posix_acl = smbd.get_sys_acl(self.tempf, smb_acl.SMB_ACL_TYPE_ACCESS) + self.assertEquals(posix_acl.count, 4) + + self.assertEquals(posix_acl.acl[0].a_type, smb_acl.SMB_ACL_USER_OBJ) + self.assertEquals(posix_acl.acl[0].a_perm, 6) + + self.assertEquals(posix_acl.acl[1].a_type, smb_acl.SMB_ACL_GROUP_OBJ) + self.assertEquals(posix_acl.acl[1].a_perm, 4) + + self.assertEquals(posix_acl.acl[2].a_type, smb_acl.SMB_ACL_OTHER) + self.assertEquals(posix_acl.acl[2].a_perm, 0) + + self.assertEquals(posix_acl.acl[3].a_type, smb_acl.SMB_ACL_MASK) + self.assertEquals(posix_acl.acl[3].a_perm, 7) + + def test_setposixacl_dir_getposixacl(self): + smbd.set_simple_acl(self.tempdir, 0750) + posix_acl = smbd.get_sys_acl(self.tempdir, smb_acl.SMB_ACL_TYPE_ACCESS) + self.assertEquals(posix_acl.count, 4) + + self.assertEquals(posix_acl.acl[0].a_type, smb_acl.SMB_ACL_USER_OBJ) + self.assertEquals(posix_acl.acl[0].a_perm, 7) + + self.assertEquals(posix_acl.acl[1].a_type, smb_acl.SMB_ACL_GROUP_OBJ) + self.assertEquals(posix_acl.acl[1].a_perm, 5) + + self.assertEquals(posix_acl.acl[2].a_type, smb_acl.SMB_ACL_OTHER) + self.assertEquals(posix_acl.acl[2].a_perm, 0) + + self.assertEquals(posix_acl.acl[3].a_type, smb_acl.SMB_ACL_MASK) + self.assertEquals(posix_acl.acl[3].a_perm, 7) + + def test_setposixacl_group_getposixacl(self): + BA_sid = security.dom_sid(security.SID_BUILTIN_ADMINISTRATORS) + s4_passdb = passdb.PDB(self.lp.get("passdb backend")) + (BA_gid,BA_type) = s4_passdb.sid_to_id(BA_sid) + self.assertEquals(BA_type, idmap.ID_TYPE_BOTH) + smbd.set_simple_acl(self.tempf, 0670, BA_gid) + posix_acl = smbd.get_sys_acl(self.tempf, smb_acl.SMB_ACL_TYPE_ACCESS) + + self.assertEquals(posix_acl.count, 5) + + self.assertEquals(posix_acl.acl[0].a_type, smb_acl.SMB_ACL_USER_OBJ) + self.assertEquals(posix_acl.acl[0].a_perm, 6) + + self.assertEquals(posix_acl.acl[1].a_type, smb_acl.SMB_ACL_GROUP_OBJ) + self.assertEquals(posix_acl.acl[1].a_perm, 7) + + self.assertEquals(posix_acl.acl[2].a_type, smb_acl.SMB_ACL_OTHER) + self.assertEquals(posix_acl.acl[2].a_perm, 0) + + self.assertEquals(posix_acl.acl[3].a_type, smb_acl.SMB_ACL_GROUP) + self.assertEquals(posix_acl.acl[3].a_perm, 7) + self.assertEquals(posix_acl.acl[3].info.gid, BA_gid) + + self.assertEquals(posix_acl.acl[4].a_type, smb_acl.SMB_ACL_MASK) + self.assertEquals(posix_acl.acl[4].a_perm, 7) + + def test_setntacl_sysvol_check_getposixacl(self): + acl = provision.SYSVOL_ACL + domsid = passdb.get_global_sam_sid() + setntacl(self.lp, self.tempf,acl,str(domsid), use_ntvfs=False) + facl = getntacl(self.lp, self.tempf) + self.assertEquals(facl.as_sddl(domsid),acl) + posix_acl = smbd.get_sys_acl(self.tempf, smb_acl.SMB_ACL_TYPE_ACCESS) + + LA_sid = security.dom_sid(str(domsid)+"-"+str(security.DOMAIN_RID_ADMINISTRATOR)) + BA_sid = security.dom_sid(security.SID_BUILTIN_ADMINISTRATORS) + SO_sid = security.dom_sid(security.SID_BUILTIN_SERVER_OPERATORS) + SY_sid = security.dom_sid(security.SID_NT_SYSTEM) + AU_sid = security.dom_sid(security.SID_NT_AUTHENTICATED_USERS) + + s4_passdb = passdb.PDB(self.lp.get("passdb backend")) + + # These assertions correct for current plugin_s4_dc selftest + # configuration. When other environments have a broad range of + # groups mapped via passdb, we can relax some of these checks + (LA_uid,LA_type) = s4_passdb.sid_to_id(LA_sid) + self.assertEquals(LA_type, idmap.ID_TYPE_UID) + (BA_gid,BA_type) = s4_passdb.sid_to_id(BA_sid) + self.assertEquals(BA_type, idmap.ID_TYPE_BOTH) + (SO_gid,SO_type) = s4_passdb.sid_to_id(SO_sid) + self.assertEquals(SO_type, idmap.ID_TYPE_BOTH) + (SY_gid,SY_type) = s4_passdb.sid_to_id(SY_sid) + self.assertEquals(SO_type, idmap.ID_TYPE_BOTH) + (AU_gid,AU_type) = s4_passdb.sid_to_id(AU_sid) + self.assertEquals(AU_type, idmap.ID_TYPE_BOTH) + + self.assertEquals(posix_acl.count, 9) + + self.assertEquals(posix_acl.acl[0].a_type, smb_acl.SMB_ACL_GROUP) + self.assertEquals(posix_acl.acl[0].a_perm, 7) + self.assertEquals(posix_acl.acl[0].info.gid, BA_gid) + + self.assertEquals(posix_acl.acl[1].a_type, smb_acl.SMB_ACL_USER) + self.assertEquals(posix_acl.acl[1].a_perm, 6) + self.assertEquals(posix_acl.acl[1].info.uid, LA_uid) + + self.assertEquals(posix_acl.acl[2].a_type, smb_acl.SMB_ACL_OTHER) + self.assertEquals(posix_acl.acl[2].a_perm, 0) + + self.assertEquals(posix_acl.acl[3].a_type, smb_acl.SMB_ACL_USER_OBJ) + self.assertEquals(posix_acl.acl[3].a_perm, 6) + + self.assertEquals(posix_acl.acl[4].a_type, smb_acl.SMB_ACL_GROUP_OBJ) + self.assertEquals(posix_acl.acl[4].a_perm, 7) + + self.assertEquals(posix_acl.acl[5].a_type, smb_acl.SMB_ACL_GROUP) + self.assertEquals(posix_acl.acl[5].a_perm, 5) + self.assertEquals(posix_acl.acl[5].info.gid, SO_gid) + + self.assertEquals(posix_acl.acl[6].a_type, smb_acl.SMB_ACL_GROUP) + self.assertEquals(posix_acl.acl[6].a_perm, 7) + self.assertEquals(posix_acl.acl[6].info.gid, SY_gid) + + self.assertEquals(posix_acl.acl[7].a_type, smb_acl.SMB_ACL_GROUP) + self.assertEquals(posix_acl.acl[7].a_perm, 5) + self.assertEquals(posix_acl.acl[7].info.gid, AU_gid) + + self.assertEquals(posix_acl.acl[8].a_type, smb_acl.SMB_ACL_MASK) + self.assertEquals(posix_acl.acl[8].a_perm, 7) + + +# check that it matches: +# user::rwx +# user:root:rwx (selftest user actually) +# group::rwx +# group:Local Admins:rwx +# group:3000000:r-x +# group:3000001:rwx +# group:3000002:r-x +# mask::rwx +# other::--- + +# +# This is in this order in the NDR smb_acl (not re-orderded for display) +# a_type: GROUP +# a_perm: 7 +# uid: -1 +# gid: 10 +# a_type: USER +# a_perm: 6 +# uid: 0 (selftest user actually) +# gid: -1 +# a_type: OTHER +# a_perm: 0 +# uid: -1 +# gid: -1 +# a_type: USER_OBJ +# a_perm: 6 +# uid: -1 +# gid: -1 +# a_type: GROUP_OBJ +# a_perm: 7 +# uid: -1 +# gid: -1 +# a_type: GROUP +# a_perm: 5 +# uid: -1 +# gid: 3000020 +# a_type: GROUP +# a_perm: 7 +# uid: -1 +# gid: 3000000 +# a_type: GROUP +# a_perm: 5 +# uid: -1 +# gid: 3000001 +# a_type: MASK +# a_perm: 7 +# uid: -1 +# gid: -1 + +# + + + def test_setntacl_sysvol_dir_check_getposixacl(self): + acl = provision.SYSVOL_ACL + domsid = passdb.get_global_sam_sid() + setntacl(self.lp, self.tempdir,acl,str(domsid), use_ntvfs=False) + facl = getntacl(self.lp, self.tempdir) + self.assertEquals(facl.as_sddl(domsid),acl) + posix_acl = smbd.get_sys_acl(self.tempdir, smb_acl.SMB_ACL_TYPE_ACCESS) + + LA_sid = security.dom_sid(str(domsid)+"-"+str(security.DOMAIN_RID_ADMINISTRATOR)) + BA_sid = security.dom_sid(security.SID_BUILTIN_ADMINISTRATORS) + SO_sid = security.dom_sid(security.SID_BUILTIN_SERVER_OPERATORS) + SY_sid = security.dom_sid(security.SID_NT_SYSTEM) + AU_sid = security.dom_sid(security.SID_NT_AUTHENTICATED_USERS) + + s4_passdb = passdb.PDB(self.lp.get("passdb backend")) + + # These assertions correct for current plugin_s4_dc selftest + # configuration. When other environments have a broad range of + # groups mapped via passdb, we can relax some of these checks + (LA_uid,LA_type) = s4_passdb.sid_to_id(LA_sid) + self.assertEquals(LA_type, idmap.ID_TYPE_UID) + (BA_gid,BA_type) = s4_passdb.sid_to_id(BA_sid) + self.assertEquals(BA_type, idmap.ID_TYPE_BOTH) + (SO_gid,SO_type) = s4_passdb.sid_to_id(SO_sid) + self.assertEquals(SO_type, idmap.ID_TYPE_BOTH) + (SY_gid,SY_type) = s4_passdb.sid_to_id(SY_sid) + self.assertEquals(SO_type, idmap.ID_TYPE_BOTH) + (AU_gid,AU_type) = s4_passdb.sid_to_id(AU_sid) + self.assertEquals(AU_type, idmap.ID_TYPE_BOTH) + + self.assertEquals(posix_acl.count, 9) + + self.assertEquals(posix_acl.acl[0].a_type, smb_acl.SMB_ACL_GROUP) + self.assertEquals(posix_acl.acl[0].a_perm, 7) + self.assertEquals(posix_acl.acl[0].info.gid, BA_gid) + + self.assertEquals(posix_acl.acl[1].a_type, smb_acl.SMB_ACL_USER) + self.assertEquals(posix_acl.acl[1].a_perm, 7) + self.assertEquals(posix_acl.acl[1].info.uid, LA_uid) + + self.assertEquals(posix_acl.acl[2].a_type, smb_acl.SMB_ACL_OTHER) + self.assertEquals(posix_acl.acl[2].a_perm, 0) + + self.assertEquals(posix_acl.acl[3].a_type, smb_acl.SMB_ACL_USER_OBJ) + self.assertEquals(posix_acl.acl[3].a_perm, 7) + + self.assertEquals(posix_acl.acl[4].a_type, smb_acl.SMB_ACL_GROUP_OBJ) + self.assertEquals(posix_acl.acl[4].a_perm, 7) + + self.assertEquals(posix_acl.acl[5].a_type, smb_acl.SMB_ACL_GROUP) + self.assertEquals(posix_acl.acl[5].a_perm, 5) + self.assertEquals(posix_acl.acl[5].info.gid, SO_gid) + + self.assertEquals(posix_acl.acl[6].a_type, smb_acl.SMB_ACL_GROUP) + self.assertEquals(posix_acl.acl[6].a_perm, 7) + self.assertEquals(posix_acl.acl[6].info.gid, SY_gid) + + self.assertEquals(posix_acl.acl[7].a_type, smb_acl.SMB_ACL_GROUP) + self.assertEquals(posix_acl.acl[7].a_perm, 5) + self.assertEquals(posix_acl.acl[7].info.gid, AU_gid) + + self.assertEquals(posix_acl.acl[8].a_type, smb_acl.SMB_ACL_MASK) + self.assertEquals(posix_acl.acl[8].a_perm, 7) + + +# check that it matches: +# user::rwx +# user:root:rwx (selftest user actually) +# group::rwx +# group:3000000:rwx +# group:3000001:r-x +# group:3000002:rwx +# group:3000003:r-x +# mask::rwx +# other::--- + + + def test_setntacl_policies_dir_check_getposixacl(self): + acl = provision.POLICIES_ACL + domsid = passdb.get_global_sam_sid() + setntacl(self.lp, self.tempdir,acl,str(domsid), use_ntvfs=False) + facl = getntacl(self.lp, self.tempdir) + self.assertEquals(facl.as_sddl(domsid),acl) + posix_acl = smbd.get_sys_acl(self.tempdir, smb_acl.SMB_ACL_TYPE_ACCESS) + + LA_sid = security.dom_sid(str(domsid)+"-"+str(security.DOMAIN_RID_ADMINISTRATOR)) + BA_sid = security.dom_sid(security.SID_BUILTIN_ADMINISTRATORS) + SO_sid = security.dom_sid(security.SID_BUILTIN_SERVER_OPERATORS) + SY_sid = security.dom_sid(security.SID_NT_SYSTEM) + AU_sid = security.dom_sid(security.SID_NT_AUTHENTICATED_USERS) + PA_sid = security.dom_sid(str(domsid)+"-"+str(security.DOMAIN_RID_POLICY_ADMINS)) + + s4_passdb = passdb.PDB(self.lp.get("passdb backend")) + + # These assertions correct for current plugin_s4_dc selftest + # configuration. When other environments have a broad range of + # groups mapped via passdb, we can relax some of these checks + (LA_uid,LA_type) = s4_passdb.sid_to_id(LA_sid) + self.assertEquals(LA_type, idmap.ID_TYPE_UID) + (BA_gid,BA_type) = s4_passdb.sid_to_id(BA_sid) + self.assertEquals(BA_type, idmap.ID_TYPE_BOTH) + (SO_gid,SO_type) = s4_passdb.sid_to_id(SO_sid) + self.assertEquals(SO_type, idmap.ID_TYPE_BOTH) + (SY_gid,SY_type) = s4_passdb.sid_to_id(SY_sid) + self.assertEquals(SO_type, idmap.ID_TYPE_BOTH) + (AU_gid,AU_type) = s4_passdb.sid_to_id(AU_sid) + self.assertEquals(AU_type, idmap.ID_TYPE_BOTH) + (PA_gid,PA_type) = s4_passdb.sid_to_id(PA_sid) + self.assertEquals(PA_type, idmap.ID_TYPE_BOTH) + + self.assertEquals(posix_acl.count, 10) + + self.assertEquals(posix_acl.acl[0].a_type, smb_acl.SMB_ACL_GROUP) + self.assertEquals(posix_acl.acl[0].a_perm, 7) + self.assertEquals(posix_acl.acl[0].info.gid, BA_gid) + + self.assertEquals(posix_acl.acl[1].a_type, smb_acl.SMB_ACL_USER) + self.assertEquals(posix_acl.acl[1].a_perm, 7) + self.assertEquals(posix_acl.acl[1].info.uid, LA_uid) + + self.assertEquals(posix_acl.acl[2].a_type, smb_acl.SMB_ACL_OTHER) + self.assertEquals(posix_acl.acl[2].a_perm, 0) + + self.assertEquals(posix_acl.acl[3].a_type, smb_acl.SMB_ACL_USER_OBJ) + self.assertEquals(posix_acl.acl[3].a_perm, 7) + + self.assertEquals(posix_acl.acl[4].a_type, smb_acl.SMB_ACL_GROUP_OBJ) + self.assertEquals(posix_acl.acl[4].a_perm, 7) + + self.assertEquals(posix_acl.acl[5].a_type, smb_acl.SMB_ACL_GROUP) + self.assertEquals(posix_acl.acl[5].a_perm, 5) + self.assertEquals(posix_acl.acl[5].info.gid, SO_gid) + + self.assertEquals(posix_acl.acl[6].a_type, smb_acl.SMB_ACL_GROUP) + self.assertEquals(posix_acl.acl[6].a_perm, 7) + self.assertEquals(posix_acl.acl[6].info.gid, SY_gid) + + self.assertEquals(posix_acl.acl[7].a_type, smb_acl.SMB_ACL_GROUP) + self.assertEquals(posix_acl.acl[7].a_perm, 5) + self.assertEquals(posix_acl.acl[7].info.gid, AU_gid) + + self.assertEquals(posix_acl.acl[8].a_type, smb_acl.SMB_ACL_GROUP) + self.assertEquals(posix_acl.acl[8].a_perm, 7) + self.assertEquals(posix_acl.acl[8].info.gid, PA_gid) + + self.assertEquals(posix_acl.acl[9].a_type, smb_acl.SMB_ACL_MASK) + self.assertEquals(posix_acl.acl[9].a_perm, 7) + + +# check that it matches: +# user::rwx +# user:root:rwx (selftest user actually) +# group::rwx +# group:3000000:rwx +# group:3000001:r-x +# group:3000002:rwx +# group:3000003:r-x +# group:3000004:rwx +# mask::rwx +# other::--- + + + + def test_setntacl_policies_check_getposixacl(self): + acl = provision.POLICIES_ACL + + domsid = passdb.get_global_sam_sid() + setntacl(self.lp, self.tempf, acl, str(domsid), use_ntvfs=False) + facl = getntacl(self.lp, self.tempf) + self.assertEquals(facl.as_sddl(domsid),acl) + posix_acl = smbd.get_sys_acl(self.tempf, smb_acl.SMB_ACL_TYPE_ACCESS) + + LA_sid = security.dom_sid(str(domsid)+"-"+str(security.DOMAIN_RID_ADMINISTRATOR)) + BA_sid = security.dom_sid(security.SID_BUILTIN_ADMINISTRATORS) + SO_sid = security.dom_sid(security.SID_BUILTIN_SERVER_OPERATORS) + SY_sid = security.dom_sid(security.SID_NT_SYSTEM) + AU_sid = security.dom_sid(security.SID_NT_AUTHENTICATED_USERS) + PA_sid = security.dom_sid(str(domsid)+"-"+str(security.DOMAIN_RID_POLICY_ADMINS)) + + s4_passdb = passdb.PDB(self.lp.get("passdb backend")) + + # These assertions correct for current plugin_s4_dc selftest + # configuration. When other environments have a broad range of + # groups mapped via passdb, we can relax some of these checks + (LA_uid,LA_type) = s4_passdb.sid_to_id(LA_sid) + self.assertEquals(LA_type, idmap.ID_TYPE_UID) + (BA_gid,BA_type) = s4_passdb.sid_to_id(BA_sid) + self.assertEquals(BA_type, idmap.ID_TYPE_BOTH) + (SO_gid,SO_type) = s4_passdb.sid_to_id(SO_sid) + self.assertEquals(SO_type, idmap.ID_TYPE_BOTH) + (SY_gid,SY_type) = s4_passdb.sid_to_id(SY_sid) + self.assertEquals(SO_type, idmap.ID_TYPE_BOTH) + (AU_gid,AU_type) = s4_passdb.sid_to_id(AU_sid) + self.assertEquals(AU_type, idmap.ID_TYPE_BOTH) + (PA_gid,PA_type) = s4_passdb.sid_to_id(PA_sid) + self.assertEquals(PA_type, idmap.ID_TYPE_BOTH) + + self.assertEquals(posix_acl.count, 10) + + self.assertEquals(posix_acl.acl[0].a_type, smb_acl.SMB_ACL_GROUP) + self.assertEquals(posix_acl.acl[0].a_perm, 7) + self.assertEquals(posix_acl.acl[0].info.gid, BA_gid) + + self.assertEquals(posix_acl.acl[1].a_type, smb_acl.SMB_ACL_USER) + self.assertEquals(posix_acl.acl[1].a_perm, 6) + self.assertEquals(posix_acl.acl[1].info.uid, LA_uid) + + self.assertEquals(posix_acl.acl[2].a_type, smb_acl.SMB_ACL_OTHER) + self.assertEquals(posix_acl.acl[2].a_perm, 0) + + self.assertEquals(posix_acl.acl[3].a_type, smb_acl.SMB_ACL_USER_OBJ) + self.assertEquals(posix_acl.acl[3].a_perm, 6) + + self.assertEquals(posix_acl.acl[4].a_type, smb_acl.SMB_ACL_GROUP_OBJ) + self.assertEquals(posix_acl.acl[4].a_perm, 7) + + self.assertEquals(posix_acl.acl[5].a_type, smb_acl.SMB_ACL_GROUP) + self.assertEquals(posix_acl.acl[5].a_perm, 5) + self.assertEquals(posix_acl.acl[5].info.gid, SO_gid) + + self.assertEquals(posix_acl.acl[6].a_type, smb_acl.SMB_ACL_GROUP) + self.assertEquals(posix_acl.acl[6].a_perm, 7) + self.assertEquals(posix_acl.acl[6].info.gid, SY_gid) + + self.assertEquals(posix_acl.acl[7].a_type, smb_acl.SMB_ACL_GROUP) + self.assertEquals(posix_acl.acl[7].a_perm, 5) + self.assertEquals(posix_acl.acl[7].info.gid, AU_gid) + + self.assertEquals(posix_acl.acl[8].a_type, smb_acl.SMB_ACL_GROUP) + self.assertEquals(posix_acl.acl[8].a_perm, 7) + self.assertEquals(posix_acl.acl[8].info.gid, PA_gid) + + self.assertEquals(posix_acl.acl[9].a_type, smb_acl.SMB_ACL_MASK) + self.assertEquals(posix_acl.acl[9].a_perm, 7) + + +# check that it matches: +# user::rwx +# user:root:rwx (selftest user actually) +# group::rwx +# group:Local Admins:rwx +# group:3000000:r-x +# group:3000001:rwx +# group:3000002:r-x +# group:3000003:rwx +# mask::rwx +# other::--- + +# +# This is in this order in the NDR smb_acl (not re-orderded for display) +# a_type: GROUP +# a_perm: 7 +# uid: -1 +# gid: 10 +# a_type: USER +# a_perm: 6 +# uid: 0 (selftest user actually) +# gid: -1 +# a_type: OTHER +# a_perm: 0 +# uid: -1 +# gid: -1 +# a_type: USER_OBJ +# a_perm: 6 +# uid: -1 +# gid: -1 +# a_type: GROUP_OBJ +# a_perm: 7 +# uid: -1 +# gid: -1 +# a_type: GROUP +# a_perm: 5 +# uid: -1 +# gid: 3000020 +# a_type: GROUP +# a_perm: 7 +# uid: -1 +# gid: 3000000 +# a_type: GROUP +# a_perm: 5 +# uid: -1 +# gid: 3000001 +# a_type: GROUP +# a_perm: 7 +# uid: -1 +# gid: 3000003 +# a_type: MASK +# a_perm: 7 +# uid: -1 +# gid: -1 + +# + + def setUp(self): + super(PosixAclMappingTests, self).setUp() + s3conf = s3param.get_context() + s3conf.load(self.get_loadparm().configfile) + s3conf.set("xattr_tdb:file", os.path.join(self.tempdir,"xattr.tdb")) + self.lp = s3conf + self.tempf = os.path.join(self.tempdir, "test") + open(self.tempf, 'w').write("empty") + + def tearDown(self): + smbd.unlink(self.tempf) + os.unlink(os.path.join(self.tempdir,"xattr.tdb")) + super(PosixAclMappingTests, self).tearDown() diff --git a/python/samba/tests/provision.py b/python/samba/tests/provision.py new file mode 100644 index 00000000000..929e7074f75 --- /dev/null +++ b/python/samba/tests/provision.py @@ -0,0 +1,203 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2012 +# +# 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/>. +# + +"""Tests for samba.provision.""" + +import os +from samba.provision import ( + ProvisionNames, + ProvisionPaths, + ProvisionResult, + determine_netbios_name, + sanitize_server_role, + setup_secretsdb, + findnss, + ) +import samba.tests +from samba.tests import env_loadparm, TestCase + +def create_dummy_secretsdb(path, lp=None): + """Create a dummy secrets database for use in tests. + + :param path: Path to store the secrets db + :param lp: Optional loadparm context. A simple one will + be generated if not specified. + """ + if lp is None: + lp = env_loadparm() + paths = ProvisionPaths() + paths.secrets = path + paths.private_dir = os.path.dirname(path) + paths.keytab = "no.keytab" + paths.dns_keytab = "no.dns.keytab" + secrets_ldb = setup_secretsdb(paths, None, None, lp=lp) + secrets_ldb.transaction_commit() + return secrets_ldb + + +class ProvisionTestCase(samba.tests.TestCaseInTempDir): + """Some simple tests for individual functions in the provisioning code. + """ + + def test_setup_secretsdb(self): + path = os.path.join(self.tempdir, "secrets.ldb") + paths = ProvisionPaths() + secrets_tdb_path = os.path.join(self.tempdir, "secrets.tdb") + secrets_ntdb_path = os.path.join(self.tempdir, "secrets.ntdb") + paths.secrets = path + paths.private_dir = os.path.dirname(path) + paths.keytab = "no.keytab" + paths.dns_keytab = "no.dns.keytab" + ldb = setup_secretsdb(paths, None, None, lp=env_loadparm()) + try: + self.assertEquals("LSA Secrets", + ldb.searchone(basedn="CN=LSA Secrets", attribute="CN")) + finally: + del ldb + os.unlink(path) + if os.path.exists(secrets_tdb_path): + os.unlink(secrets_tdb_path) + if os.path.exists(secrets_ntdb_path): + os.unlink(secrets_ntdb_path) + +class FindNssTests(TestCase): + """Test findnss() function.""" + + def test_nothing(self): + def x(y): + raise KeyError + self.assertRaises(KeyError, findnss, x, []) + + def test_first(self): + self.assertEquals("bla", findnss(lambda x: "bla", ["bla"])) + + def test_skip_first(self): + def x(y): + if y != "bla": + raise KeyError + return "ha" + self.assertEquals("ha", findnss(x, ["bloe", "bla"])) + + +class Disabled(object): + + def test_setup_templatesdb(self): + raise NotImplementedError(self.test_setup_templatesdb) + + def test_setup_registry(self): + raise NotImplementedError(self.test_setup_registry) + + def test_setup_samdb_rootdse(self): + raise NotImplementedError(self.test_setup_samdb_rootdse) + + def test_setup_samdb_partitions(self): + raise NotImplementedError(self.test_setup_samdb_partitions) + + def test_provision_dns(self): + raise NotImplementedError(self.test_provision_dns) + + def test_provision_ldapbase(self): + raise NotImplementedError(self.test_provision_ldapbase) + + def test_provision_guess(self): + raise NotImplementedError(self.test_provision_guess) + + def test_join_domain(self): + raise NotImplementedError(self.test_join_domain) + + def test_vampire(self): + raise NotImplementedError(self.test_vampire) + + +class SanitizeServerRoleTests(TestCase): + + def test_same(self): + self.assertEquals("standalone server", + sanitize_server_role("standalone server")) + self.assertEquals("member server", + sanitize_server_role("member server")) + + def test_invalid(self): + self.assertRaises(ValueError, sanitize_server_role, "foo") + + def test_valid(self): + self.assertEquals( + "standalone server", + sanitize_server_role("ROLE_STANDALONE")) + self.assertEquals( + "standalone server", + sanitize_server_role("standalone")) + self.assertEquals( + "active directory domain controller", + sanitize_server_role("domain controller")) + + +class DummyLogger(object): + + def __init__(self): + self.entries = [] + + def info(self, text, *args): + self.entries.append(("INFO", text % args)) + + +class ProvisionResultTests(TestCase): + + def report_logger(self, result): + logger = DummyLogger() + result.report_logger(logger) + return logger.entries + + def base_result(self): + result = ProvisionResult() + result.server_role = "domain controller" + result.names = ProvisionNames() + result.names.hostname = "hostnaam" + result.names.domain = "DOMEIN" + result.names.dnsdomain = "dnsdomein" + result.domainsid = "S1-1-1" + result.paths = ProvisionPaths() + return result + + def test_basic_report_logger(self): + result = self.base_result() + entries = self.report_logger(result) + self.assertEquals(entries, [ + ('INFO', 'Once the above files are installed, your Samba4 server ' + 'will be ready to use'), + ('INFO', 'Server Role: domain controller'), + ('INFO', 'Hostname: hostnaam'), + ('INFO', 'NetBIOS Domain: DOMEIN'), + ('INFO', 'DNS Domain: dnsdomein'), + ('INFO', 'DOMAIN SID: S1-1-1')]) + + def test_report_logger_adminpass(self): + result = self.base_result() + result.adminpass_generated = True + result.adminpass = "geheim" + entries = self.report_logger(result) + self.assertEquals(entries[1], + ("INFO", 'Admin password: geheim')) + + +class DetermineNetbiosNameTests(TestCase): + + def test_limits_to_15(self): + self.assertEquals("A" * 15, determine_netbios_name("a" * 30)) + + def test_strips_invalid(self): + self.assertEquals("BLABLA", determine_netbios_name("bla/bla")) diff --git a/python/samba/tests/registry.py b/python/samba/tests/registry.py new file mode 100644 index 00000000000..8016a0bb686 --- /dev/null +++ b/python/samba/tests/registry.py @@ -0,0 +1,60 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007 +# +# 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/>. +# + +"""Tests for samba.registry.""" + +import os +from samba import registry +import samba.tests + +class HelperTests(samba.tests.TestCase): + + def test_predef_to_name(self): + self.assertEquals("HKEY_LOCAL_MACHINE", + registry.get_predef_name(0x80000002)) + + def test_str_regtype(self): + self.assertEquals("REG_DWORD", registry.str_regtype(4)) + + + +class HiveTests(samba.tests.TestCaseInTempDir): + + def setUp(self): + super(HiveTests, self).setUp() + self.hive_path = os.path.join(self.tempdir, "ldb_new.ldb") + self.hive = registry.open_ldb(self.hive_path) + + def tearDown(self): + del self.hive + os.unlink(self.hive_path) + super(HiveTests, self).tearDown() + + def test_ldb_new(self): + self.assertTrue(self.hive is not None) + + #def test_flush(self): + # self.hive.flush() + + #def test_del_value(self): + # self.hive.del_value("FOO") + + +class RegistryTests(samba.tests.TestCase): + + def test_new(self): + self.registry = registry.Registry() diff --git a/python/samba/tests/samba3.py b/python/samba/tests/samba3.py new file mode 100644 index 00000000000..0a7f13c66fa --- /dev/null +++ b/python/samba/tests/samba3.py @@ -0,0 +1,219 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007 +# +# 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/>. +# + +"""Tests for samba.samba3.""" + +from samba.samba3 import ( + Registry, + WinsDatabase, + IdmapDatabase, + ) +from samba.samba3 import passdb +from samba.samba3 import param as s3param +from samba.tests import TestCase, TestCaseInTempDir +from samba.dcerpc.security import dom_sid +import os + + +for p in [ "../../../../../testdata/samba3", "../../../../testdata/samba3" ]: + DATADIR = os.path.join(os.path.dirname(__file__), p) + if os.path.exists(DATADIR): + break + + +class RegistryTestCase(TestCase): + + def setUp(self): + super(RegistryTestCase, self).setUp() + self.registry = Registry(os.path.join(DATADIR, "registry.tdb")) + + def tearDown(self): + self.registry.close() + super(RegistryTestCase, self).tearDown() + + def test_length(self): + self.assertEquals(28, len(self.registry)) + + def test_keys(self): + self.assertTrue("HKLM" in self.registry.keys()) + + def test_subkeys(self): + self.assertEquals(["SOFTWARE", "SYSTEM"], self.registry.subkeys("HKLM")) + + def test_values(self): + self.assertEquals({'DisplayName': (1L, 'E\x00v\x00e\x00n\x00t\x00 \x00L\x00o\x00g\x00\x00\x00'), + 'ErrorControl': (4L, '\x01\x00\x00\x00')}, + self.registry.values("HKLM/SYSTEM/CURRENTCONTROLSET/SERVICES/EVENTLOG")) + + +class PassdbTestCase(TestCaseInTempDir): + + def setUp(self): + super (PassdbTestCase, self).setUp() + os.system("cp -r %s %s" % (DATADIR, self.tempdir)) + datadir = os.path.join(self.tempdir, "samba3") + + self.lp = s3param.get_context() + self.lp.load(os.path.join(datadir, "smb.conf")) + self.lp.set("private dir", datadir) + self.lp.set("state directory", datadir) + self.lp.set("lock directory", datadir) + passdb.set_secrets_dir(datadir) + self.pdb = passdb.PDB("tdbsam") + + def tearDown(self): + self.lp = [] + self.pdb = [] + os.system("rm -rf %s" % os.path.join(self.tempdir, "samba3")) + super(PassdbTestCase, self).tearDown() + + def test_param(self): + self.assertEquals("BEDWYR", self.lp.get("netbios name")) + self.assertEquals("SAMBA", self.lp.get("workgroup")) + self.assertEquals("USER", self.lp.get("security")) + + def test_policy(self): + policy = self.pdb.get_account_policy() + self.assertEquals(0, policy['bad lockout attempt']) + self.assertEquals(-1, policy['disconnect time']) + self.assertEquals(0, policy['lockout duration']) + self.assertEquals(999999999, policy['maximum password age']) + self.assertEquals(0, policy['minimum password age']) + self.assertEquals(5, policy['min password length']) + self.assertEquals(0, policy['password history']) + self.assertEquals(0, policy['refuse machine password change']) + self.assertEquals(0, policy['reset count minutes']) + self.assertEquals(0, policy['user must logon to change password']) + + def test_get_sid(self): + domain_sid = passdb.get_global_sam_sid() + self.assertEquals(dom_sid("S-1-5-21-2470180966-3899876309-2637894779"), domain_sid) + + def test_usernames(self): + userlist = self.pdb.search_users(0) + self.assertEquals(3, len(userlist)) + + def test_getuser(self): + user = self.pdb.getsampwnam("root") + + self.assertEquals(16, user.acct_ctrl) + self.assertEquals("", user.acct_desc) + self.assertEquals(0, user.bad_password_count) + self.assertEquals(0, user.bad_password_time) + self.assertEquals(0, user.code_page) + self.assertEquals(0, user.country_code) + self.assertEquals("", user.dir_drive) + self.assertEquals("BEDWYR", user.domain) + self.assertEquals("root", user.full_name) + self.assertEquals(dom_sid('S-1-5-21-2470180966-3899876309-2637894779-513'), user.group_sid) + self.assertEquals("\\\\BEDWYR\\root", user.home_dir) + self.assertEquals([-1 for i in range(21)], user.hours) + self.assertEquals(21, user.hours_len) + self.assertEquals(9223372036854775807, user.kickoff_time) + self.assertEquals(None, user.lanman_passwd) + self.assertEquals(9223372036854775807, user.logoff_time) + self.assertEquals(0, user.logon_count) + self.assertEquals(168, user.logon_divs) + self.assertEquals("", user.logon_script) + self.assertEquals(0, user.logon_time) + self.assertEquals("", user.munged_dial) + self.assertEquals('\x87\x8d\x80\x14`l\xda)gzD\xef\xa15?\xc7', user.nt_passwd) + self.assertEquals("", user.nt_username) + self.assertEquals(1125418267, user.pass_can_change_time) + self.assertEquals(1125418267, user.pass_last_set_time) + self.assertEquals(2125418266, user.pass_must_change_time) + self.assertEquals(None, user.plaintext_passwd) + self.assertEquals("\\\\BEDWYR\\root\\profile", user.profile_path) + self.assertEquals(None, user.pw_history) + self.assertEquals(dom_sid("S-1-5-21-2470180966-3899876309-2637894779-1000"), user.user_sid) + self.assertEquals("root", user.username) + self.assertEquals("", user.workstations) + + def test_group_length(self): + grouplist = self.pdb.enum_group_mapping() + self.assertEquals(13, len(grouplist)) + + def test_get_group(self): + group = self.pdb.getgrsid(dom_sid("S-1-5-32-544")) + self.assertEquals("Administrators", group.nt_name) + self.assertEquals(-1, group.gid) + self.assertEquals(5, group.sid_name_use) + + def test_groupsids(self): + grouplist = self.pdb.enum_group_mapping() + sids = [] + for g in grouplist: + sids.append(str(g.sid)) + self.assertTrue("S-1-5-32-544" in sids) + self.assertTrue("S-1-5-32-545" in sids) + self.assertTrue("S-1-5-32-546" in sids) + self.assertTrue("S-1-5-32-548" in sids) + self.assertTrue("S-1-5-32-549" in sids) + self.assertTrue("S-1-5-32-550" in sids) + self.assertTrue("S-1-5-32-551" in sids) + + def test_alias_length(self): + aliaslist = self.pdb.search_aliases() + self.assertEquals(1, len(aliaslist)) + self.assertEquals("Jelmers NT Group", aliaslist[0]['account_name']) + + +class WinsDatabaseTestCase(TestCase): + + def setUp(self): + super(WinsDatabaseTestCase, self).setUp() + self.winsdb = WinsDatabase(os.path.join(DATADIR, "wins.dat")) + + def test_length(self): + self.assertEquals(22, len(self.winsdb)) + + def test_first_entry(self): + self.assertEqual((1124185120, ["192.168.1.5"], 0x64), self.winsdb["ADMINISTRATOR#03"]) + + def tearDown(self): + self.winsdb.close() + super(WinsDatabaseTestCase, self).tearDown() + + +class IdmapDbTestCase(TestCase): + + def setUp(self): + super(IdmapDbTestCase, self).setUp() + self.idmapdb = IdmapDatabase(os.path.join(DATADIR, + "winbindd_idmap.tdb")) + + def test_user_hwm(self): + self.assertEquals(10000, self.idmapdb.get_user_hwm()) + + def test_group_hwm(self): + self.assertEquals(10002, self.idmapdb.get_group_hwm()) + + def test_uids(self): + self.assertEquals(1, len(list(self.idmapdb.uids()))) + + def test_gids(self): + self.assertEquals(3, len(list(self.idmapdb.gids()))) + + def test_get_user_sid(self): + self.assertEquals("S-1-5-21-58189338-3053988021-627566699-501", self.idmapdb.get_user_sid(65534)) + + def test_get_group_sid(self): + self.assertEquals("S-1-5-21-2447931902-1787058256-3961074038-3007", self.idmapdb.get_group_sid(10001)) + + def tearDown(self): + self.idmapdb.close() + super(IdmapDbTestCase, self).tearDown() diff --git a/python/samba/tests/samba3sam.py b/python/samba/tests/samba3sam.py new file mode 100644 index 00000000000..9c017fb79c3 --- /dev/null +++ b/python/samba/tests/samba3sam.py @@ -0,0 +1,1125 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2005-2008 +# Copyright (C) Martin Kuehl <mkhl@samba.org> 2006 +# +# This is a Python port of the original in testprogs/ejs/samba3sam.js +# +# 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/>. +# + +"""Tests for the samba3sam LDB module, which maps Samba3 LDAP to AD LDAP.""" + +import os +import ldb +from ldb import SCOPE_DEFAULT, SCOPE_BASE +from samba import Ldb, substitute_var +from samba.tests import TestCaseInTempDir, env_loadparm +import samba.dcerpc.security +import samba.ndr +from samba.auth import system_session +from operator import attrgetter + + +def read_datafile(filename): + paths = [ "../../../../../testdata/samba3", + "../../../../testdata/samba3" ] + for p in paths: + datadir = os.path.join(os.path.dirname(__file__), p) + if os.path.exists(datadir): + break + return open(os.path.join(datadir, filename), 'r').read() + +def ldb_debug(l, text): + print text + + +class MapBaseTestCase(TestCaseInTempDir): + """Base test case for mapping tests.""" + + def setup_modules(self, ldb, s3, s4): + ldb.add({"dn": "@MAP=samba3sam", + "@FROM": s4.basedn, + "@TO": "sambaDomainName=TESTS," + s3.basedn}) + + ldb.add({"dn": "@MODULES", + "@LIST": "rootdse,paged_results,server_sort,asq,samldb,password_hash,operational,objectguid,rdn_name,samba3sam,samba3sid,show_deleted,partition"}) + + ldb.add({"dn": "@PARTITION", + "partition": ["%s" % (s4.basedn_casefold), + "%s" % (s3.basedn_casefold)], + "replicateEntries": ["@ATTRIBUTES", "@INDEXLIST"], + "modules": "*:"}) + + def setUp(self): + self.lp = env_loadparm() + self.lp.set("workgroup", "TESTS") + self.lp.set("netbios name", "TESTS") + super(MapBaseTestCase, self).setUp() + + def make_dn(basedn, rdn): + return "%s,sambaDomainName=TESTS,%s" % (rdn, basedn) + + def make_s4dn(basedn, rdn): + return "%s,%s" % (rdn, basedn) + + self.ldbfile = os.path.join(self.tempdir, "test.ldb") + self.ldburl = "tdb://" + self.ldbfile + + tempdir = self.tempdir + + class Target: + """Simple helper class that contains data for a specific SAM + connection.""" + + def __init__(self, basedn, dn, lp): + self.db = Ldb(lp=lp, session_info=system_session()) + self.db.set_opaque("skip_allocate_sids", "true"); + self.basedn = basedn + self.basedn_casefold = ldb.Dn(self.db, basedn).get_casefold() + self.substvars = {"BASEDN": self.basedn} + self.file = os.path.join(tempdir, "%s.ldb" % self.basedn_casefold) + self.url = "tdb://" + self.file + self._dn = dn + + def dn(self, rdn): + return self._dn(self.basedn, rdn) + + def connect(self): + return self.db.connect(self.url) + + def setup_data(self, path): + self.add_ldif(read_datafile(path)) + + def subst(self, text): + return substitute_var(text, self.substvars) + + def add_ldif(self, ldif): + self.db.add_ldif(self.subst(ldif)) + + def modify_ldif(self, ldif): + self.db.modify_ldif(self.subst(ldif)) + + self.samba4 = Target("dc=vernstok,dc=nl", make_s4dn, self.lp) + self.samba3 = Target("cn=Samba3Sam", make_dn, self.lp) + + self.samba3.connect() + self.samba4.connect() + + def tearDown(self): + os.unlink(self.ldbfile) + os.unlink(self.samba3.file) + os.unlink(self.samba4.file) + pdir = "%s.d" % self.ldbfile + mdata = os.path.join(pdir, "metadata.tdb") + if os.path.exists(mdata): + os.unlink(mdata) + os.rmdir(pdir) + super(MapBaseTestCase, self).tearDown() + + def assertSidEquals(self, text, ndr_sid): + sid_obj1 = samba.ndr.ndr_unpack(samba.dcerpc.security.dom_sid, + str(ndr_sid[0])) + sid_obj2 = samba.dcerpc.security.dom_sid(text) + self.assertEquals(sid_obj1, sid_obj2) + + +class Samba3SamTestCase(MapBaseTestCase): + + def setUp(self): + super(Samba3SamTestCase, self).setUp() + ldb = Ldb(self.ldburl, lp=self.lp, session_info=system_session()) + ldb.set_opaque("skip_allocate_sids", "true"); + self.samba3.setup_data("samba3.ldif") + ldif = read_datafile("provision_samba3sam.ldif") + ldb.add_ldif(self.samba4.subst(ldif)) + self.setup_modules(ldb, self.samba3, self.samba4) + del ldb + self.ldb = Ldb(self.ldburl, lp=self.lp, session_info=system_session()) + self.ldb.set_opaque("skip_allocate_sids", "true"); + + def test_search_non_mapped(self): + """Looking up by non-mapped attribute""" + msg = self.ldb.search(expression="(cn=Administrator)") + self.assertEquals(len(msg), 1) + self.assertEquals(msg[0]["cn"], "Administrator") + + def test_search_non_mapped(self): + """Looking up by mapped attribute""" + msg = self.ldb.search(expression="(name=Backup Operators)") + self.assertEquals(len(msg), 1) + self.assertEquals(str(msg[0]["name"]), "Backup Operators") + + def test_old_name_of_renamed(self): + """Looking up by old name of renamed attribute""" + msg = self.ldb.search(expression="(displayName=Backup Operators)") + self.assertEquals(len(msg), 0) + + def test_mapped_containing_sid(self): + """Looking up mapped entry containing SID""" + msg = self.ldb.search(expression="(cn=Replicator)") + self.assertEquals(len(msg), 1) + self.assertEquals(str(msg[0].dn), + "cn=Replicator,ou=Groups,dc=vernstok,dc=nl") + self.assertTrue("objectSid" in msg[0]) + self.assertSidEquals("S-1-5-21-4231626423-2410014848-2360679739-552", + msg[0]["objectSid"]) + oc = set(msg[0]["objectClass"]) + self.assertEquals(oc, set(["group"])) + + def test_search_by_objclass(self): + """Looking up by objectClass""" + msg = self.ldb.search(expression="(|(objectClass=user)(cn=Administrator))") + self.assertEquals(set([str(m.dn) for m in msg]), + set(["unixName=Administrator,ou=Users,dc=vernstok,dc=nl", + "unixName=nobody,ou=Users,dc=vernstok,dc=nl"])) + + def test_s3sam_modify(self): + # Adding a record that will be fallbacked + self.ldb.add({"dn": "cn=Foo", + "foo": "bar", + "blah": "Blie", + "cn": "Foo", + "showInAdvancedViewOnly": "TRUE"} + ) + + # Checking for existence of record (local) + # TODO: This record must be searched in the local database, which is + # currently only supported for base searches + # msg = ldb.search(expression="(cn=Foo)", ['foo','blah','cn','showInAdvancedViewOnly')] + # TODO: Actually, this version should work as well but doesn't... + # + # + msg = self.ldb.search(expression="(cn=Foo)", base="cn=Foo", + scope=SCOPE_BASE, + attrs=['foo','blah','cn','showInAdvancedViewOnly']) + self.assertEquals(len(msg), 1) + self.assertEquals(str(msg[0]["showInAdvancedViewOnly"]), "TRUE") + self.assertEquals(str(msg[0]["foo"]), "bar") + self.assertEquals(str(msg[0]["blah"]), "Blie") + + # Adding record that will be mapped + self.ldb.add({"dn": "cn=Niemand,cn=Users,dc=vernstok,dc=nl", + "objectClass": "user", + "unixName": "bin", + "sambaUnicodePwd": "geheim", + "cn": "Niemand"}) + + # Checking for existence of record (remote) + msg = self.ldb.search(expression="(unixName=bin)", + attrs=['unixName','cn','dn', 'sambaUnicodePwd']) + self.assertEquals(len(msg), 1) + self.assertEquals(str(msg[0]["cn"]), "Niemand") + self.assertEquals(str(msg[0]["sambaUnicodePwd"]), "geheim") + + # Checking for existence of record (local && remote) + msg = self.ldb.search(expression="(&(unixName=bin)(sambaUnicodePwd=geheim))", + attrs=['unixName','cn','dn', 'sambaUnicodePwd']) + self.assertEquals(len(msg), 1) # TODO: should check with more records + self.assertEquals(str(msg[0]["cn"]), "Niemand") + self.assertEquals(str(msg[0]["unixName"]), "bin") + self.assertEquals(str(msg[0]["sambaUnicodePwd"]), "geheim") + + # Checking for existence of record (local || remote) + msg = self.ldb.search(expression="(|(unixName=bin)(sambaUnicodePwd=geheim))", + attrs=['unixName','cn','dn', 'sambaUnicodePwd']) + #print "got %d replies" % len(msg) + self.assertEquals(len(msg), 1) # TODO: should check with more records + self.assertEquals(str(msg[0]["cn"]), "Niemand") + self.assertEquals(str(msg[0]["unixName"]), "bin") + self.assertEquals(str(msg[0]["sambaUnicodePwd"]), "geheim") + + # Checking for data in destination database + msg = self.samba3.db.search(expression="(cn=Niemand)") + self.assertTrue(len(msg) >= 1) + self.assertEquals(str(msg[0]["sambaSID"]), + "S-1-5-21-4231626423-2410014848-2360679739-2001") + self.assertEquals(str(msg[0]["displayName"]), "Niemand") + + # Adding attribute... + self.ldb.modify_ldif(""" +dn: cn=Niemand,cn=Users,dc=vernstok,dc=nl +changetype: modify +add: description +description: Blah +""") + + # Checking whether changes are still there... + msg = self.ldb.search(expression="(cn=Niemand)") + self.assertTrue(len(msg) >= 1) + self.assertEquals(str(msg[0]["cn"]), "Niemand") + self.assertEquals(str(msg[0]["description"]), "Blah") + + # Modifying attribute... + self.ldb.modify_ldif(""" +dn: cn=Niemand,cn=Users,dc=vernstok,dc=nl +changetype: modify +replace: description +description: Blie +""") + + # Checking whether changes are still there... + msg = self.ldb.search(expression="(cn=Niemand)") + self.assertTrue(len(msg) >= 1) + self.assertEquals(str(msg[0]["description"]), "Blie") + + # Deleting attribute... + self.ldb.modify_ldif(""" +dn: cn=Niemand,cn=Users,dc=vernstok,dc=nl +changetype: modify +delete: description +""") + + # Checking whether changes are no longer there... + msg = self.ldb.search(expression="(cn=Niemand)") + self.assertTrue(len(msg) >= 1) + self.assertTrue(not "description" in msg[0]) + + # Renaming record... + self.ldb.rename("cn=Niemand,cn=Users,dc=vernstok,dc=nl", + "cn=Niemand2,cn=Users,dc=vernstok,dc=nl") + + # Checking whether DN has changed... + msg = self.ldb.search(expression="(cn=Niemand2)") + self.assertEquals(len(msg), 1) + self.assertEquals(str(msg[0].dn), + "cn=Niemand2,cn=Users,dc=vernstok,dc=nl") + + # Deleting record... + self.ldb.delete("cn=Niemand2,cn=Users,dc=vernstok,dc=nl") + + # Checking whether record is gone... + msg = self.ldb.search(expression="(cn=Niemand2)") + self.assertEquals(len(msg), 0) + + +class MapTestCase(MapBaseTestCase): + + def setUp(self): + super(MapTestCase, self).setUp() + ldb = Ldb(self.ldburl, lp=self.lp, session_info=system_session()) + ldb.set_opaque("skip_allocate_sids", "true"); + ldif = read_datafile("provision_samba3sam.ldif") + ldb.add_ldif(self.samba4.subst(ldif)) + self.setup_modules(ldb, self.samba3, self.samba4) + del ldb + self.ldb = Ldb(self.ldburl, lp=self.lp, session_info=system_session()) + self.ldb.set_opaque("skip_allocate_sids", "true"); + + def test_map_search(self): + """Running search tests on mapped data.""" + self.samba3.db.add({ + "dn": "sambaDomainName=TESTS," + self.samba3.basedn, + "objectclass": ["sambaDomain", "top"], + "sambaSID": "S-1-5-21-4231626423-2410014848-2360679739", + "sambaNextRid": "2000", + "sambaDomainName": "TESTS" + }) + + # Add a set of split records + self.ldb.add_ldif(""" +dn: """+ self.samba4.dn("cn=Domain Users") + """ +objectClass: group +cn: Domain Users +objectSid: S-1-5-21-4231626423-2410014848-2360679739-513 +""") + + # Add a set of split records + self.ldb.add_ldif(""" +dn: """+ self.samba4.dn("cn=X") + """ +objectClass: user +cn: X +codePage: x +revision: x +dnsHostName: x +nextRid: y +lastLogon: x +description: x +objectSid: S-1-5-21-4231626423-2410014848-2360679739-552 +""") + + self.ldb.add({ + "dn": self.samba4.dn("cn=Y"), + "objectClass": "top", + "cn": "Y", + "codePage": "x", + "revision": "x", + "dnsHostName": "y", + "nextRid": "y", + "lastLogon": "y", + "description": "x"}) + + self.ldb.add({ + "dn": self.samba4.dn("cn=Z"), + "objectClass": "top", + "cn": "Z", + "codePage": "x", + "revision": "y", + "dnsHostName": "z", + "nextRid": "y", + "lastLogon": "z", + "description": "y"}) + + # Add a set of remote records + + self.samba3.db.add({ + "dn": self.samba3.dn("cn=A"), + "objectClass": "posixAccount", + "cn": "A", + "sambaNextRid": "x", + "sambaBadPasswordCount": "x", + "sambaLogonTime": "x", + "description": "x", + "sambaSID": "S-1-5-21-4231626423-2410014848-2360679739-552", + "sambaPrimaryGroupSID": "S-1-5-21-4231626423-2410014848-2360679739-512"}) + + self.samba3.db.add({ + "dn": self.samba3.dn("cn=B"), + "objectClass": "top", + "cn": "B", + "sambaNextRid": "x", + "sambaBadPasswordCount": "x", + "sambaLogonTime": "y", + "description": "x"}) + + self.samba3.db.add({ + "dn": self.samba3.dn("cn=C"), + "objectClass": "top", + "cn": "C", + "sambaNextRid": "x", + "sambaBadPasswordCount": "y", + "sambaLogonTime": "z", + "description": "y"}) + + # Testing search by DN + + # Search remote record by local DN + dn = self.samba4.dn("cn=A") + res = self.ldb.search(dn, scope=SCOPE_BASE, + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertEquals(str(res[0]["lastLogon"]), "x") + + # Search remote record by remote DN + dn = self.samba3.dn("cn=A") + res = self.samba3.db.search(dn, scope=SCOPE_BASE, + attrs=["dnsHostName", "lastLogon", "sambaLogonTime"]) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertTrue(not "lastLogon" in res[0]) + self.assertEquals(str(res[0]["sambaLogonTime"]), "x") + + # Search split record by local DN + dn = self.samba4.dn("cn=X") + res = self.ldb.search(dn, scope=SCOPE_BASE, + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertEquals(str(res[0]["dnsHostName"]), "x") + self.assertEquals(str(res[0]["lastLogon"]), "x") + + # Search split record by remote DN + dn = self.samba3.dn("cn=X") + res = self.samba3.db.search(dn, scope=SCOPE_BASE, + attrs=["dnsHostName", "lastLogon", "sambaLogonTime"]) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertTrue(not "lastLogon" in res[0]) + self.assertEquals(str(res[0]["sambaLogonTime"]), "x") + + # Testing search by attribute + + # Search by ignored attribute + res = self.ldb.search(expression="(revision=x)", scope=SCOPE_DEFAULT, + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 2) + res = sorted(res, key=attrgetter('dn')) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=X")) + self.assertEquals(str(res[0]["dnsHostName"]), "x") + self.assertEquals(str(res[0]["lastLogon"]), "x") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=Y")) + self.assertEquals(str(res[1]["dnsHostName"]), "y") + self.assertEquals(str(res[1]["lastLogon"]), "y") + + # Search by kept attribute + res = self.ldb.search(expression="(description=y)", + scope=SCOPE_DEFAULT, attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 2) + res = sorted(res, key=attrgetter('dn')) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=C")) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertEquals(str(res[0]["lastLogon"]), "z") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=Z")) + self.assertEquals(str(res[1]["dnsHostName"]), "z") + self.assertEquals(str(res[1]["lastLogon"]), "z") + + # Search by renamed attribute + res = self.ldb.search(expression="(badPwdCount=x)", scope=SCOPE_DEFAULT, + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 2) + res = sorted(res, key=attrgetter('dn')) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=A")) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertEquals(str(res[0]["lastLogon"]), "x") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=B")) + self.assertTrue(not "dnsHostName" in res[1]) + self.assertEquals(str(res[1]["lastLogon"]), "y") + + # Search by converted attribute + # TODO: + # Using the SID directly in the parse tree leads to conversion + # errors, letting the search fail with no results. + #res = self.ldb.search("(objectSid=S-1-5-21-4231626423-2410014848-2360679739-552)", scope=SCOPE_DEFAULT, attrs) + res = self.ldb.search(expression="(objectSid=*)", base=None, scope=SCOPE_DEFAULT, attrs=["dnsHostName", "lastLogon", "objectSid"]) + self.assertEquals(len(res), 4) + res = sorted(res, key=attrgetter('dn')) + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=X")) + self.assertEquals(str(res[1]["dnsHostName"]), "x") + self.assertEquals(str(res[1]["lastLogon"]), "x") + self.assertSidEquals("S-1-5-21-4231626423-2410014848-2360679739-552", + res[1]["objectSid"]) + self.assertTrue("objectSid" in res[1]) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=A")) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertEquals(str(res[0]["lastLogon"]), "x") + self.assertSidEquals("S-1-5-21-4231626423-2410014848-2360679739-552", + res[0]["objectSid"]) + self.assertTrue("objectSid" in res[0]) + + # Search by generated attribute + # In most cases, this even works when the mapping is missing + # a `convert_operator' by enumerating the remote db. + res = self.ldb.search(expression="(primaryGroupID=512)", + attrs=["dnsHostName", "lastLogon", "primaryGroupID"]) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=A")) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertEquals(str(res[0]["lastLogon"]), "x") + self.assertEquals(str(res[0]["primaryGroupID"]), "512") + + # Note that Xs "objectSid" seems to be fine in the previous search for + # "objectSid"... + #res = ldb.search(expression="(primaryGroupID=*)", NULL, ldb. SCOPE_DEFAULT, attrs) + #print len(res) + " results found" + #for i in range(len(res)): + # for (obj in res[i]) { + # print obj + ": " + res[i][obj] + # } + # print "---" + # + + # Search by remote name of renamed attribute */ + res = self.ldb.search(expression="(sambaBadPasswordCount=*)", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 0) + + # Search by objectClass + attrs = ["dnsHostName", "lastLogon", "objectClass"] + res = self.ldb.search(expression="(objectClass=user)", attrs=attrs) + self.assertEquals(len(res), 2) + res = sorted(res, key=attrgetter('dn')) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=A")) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertEquals(str(res[0]["lastLogon"]), "x") + self.assertEquals(str(res[0]["objectClass"][0]), "user") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=X")) + self.assertEquals(str(res[1]["dnsHostName"]), "x") + self.assertEquals(str(res[1]["lastLogon"]), "x") + self.assertEquals(str(res[1]["objectClass"][0]), "user") + + # Prove that the objectClass is actually used for the search + res = self.ldb.search(expression="(|(objectClass=user)(badPwdCount=x))", + attrs=attrs) + self.assertEquals(len(res), 3) + res = sorted(res, key=attrgetter('dn')) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=A")) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertEquals(str(res[0]["lastLogon"]), "x") + self.assertEquals(res[0]["objectClass"][0], "user") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=B")) + self.assertTrue(not "dnsHostName" in res[1]) + self.assertEquals(str(res[1]["lastLogon"]), "y") + self.assertEquals(set(res[1]["objectClass"]), set(["top"])) + self.assertEquals(str(res[2].dn), self.samba4.dn("cn=X")) + self.assertEquals(str(res[2]["dnsHostName"]), "x") + self.assertEquals(str(res[2]["lastLogon"]), "x") + self.assertEquals(str(res[2]["objectClass"][0]), "user") + + # Testing search by parse tree + + # Search by conjunction of local attributes + res = self.ldb.search(expression="(&(codePage=x)(revision=x))", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 2) + res = sorted(res, key=attrgetter('dn')) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=X")) + self.assertEquals(str(res[0]["dnsHostName"]), "x") + self.assertEquals(str(res[0]["lastLogon"]), "x") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=Y")) + self.assertEquals(str(res[1]["dnsHostName"]), "y") + self.assertEquals(str(res[1]["lastLogon"]), "y") + + # Search by conjunction of remote attributes + res = self.ldb.search(expression="(&(lastLogon=x)(description=x))", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 2) + res = sorted(res, key=attrgetter('dn')) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=A")) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertEquals(str(res[0]["lastLogon"]), "x") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=X")) + self.assertEquals(str(res[1]["dnsHostName"]), "x") + self.assertEquals(str(res[1]["lastLogon"]), "x") + + # Search by conjunction of local and remote attribute + res = self.ldb.search(expression="(&(codePage=x)(description=x))", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 2) + res = sorted(res, key=attrgetter('dn')) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=X")) + self.assertEquals(str(res[0]["dnsHostName"]), "x") + self.assertEquals(str(res[0]["lastLogon"]), "x") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=Y")) + self.assertEquals(str(res[1]["dnsHostName"]), "y") + self.assertEquals(str(res[1]["lastLogon"]), "y") + + # Search by conjunction of local and remote attribute w/o match + attrs = ["dnsHostName", "lastLogon"] + res = self.ldb.search(expression="(&(codePage=x)(nextRid=x))", + attrs=attrs) + self.assertEquals(len(res), 0) + res = self.ldb.search(expression="(&(revision=x)(lastLogon=z))", + attrs=attrs) + self.assertEquals(len(res), 0) + + # Search by disjunction of local attributes + res = self.ldb.search(expression="(|(revision=x)(dnsHostName=x))", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 2) + res = sorted(res, key=attrgetter('dn')) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=X")) + self.assertEquals(str(res[0]["dnsHostName"]), "x") + self.assertEquals(str(res[0]["lastLogon"]), "x") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=Y")) + self.assertEquals(str(res[1]["dnsHostName"]), "y") + self.assertEquals(str(res[1]["lastLogon"]), "y") + + # Search by disjunction of remote attributes + res = self.ldb.search(expression="(|(badPwdCount=x)(lastLogon=x))", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 3) + res = sorted(res, key=attrgetter('dn')) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=A")) + self.assertFalse("dnsHostName" in res[0]) + self.assertEquals(str(res[0]["lastLogon"]), "x") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=B")) + self.assertFalse("dnsHostName" in res[1]) + self.assertEquals(str(res[1]["lastLogon"]), "y") + self.assertEquals(str(res[2].dn), self.samba4.dn("cn=X")) + self.assertEquals(str(res[2]["dnsHostName"]), "x") + self.assertEquals(str(res[2]["lastLogon"]), "x") + + # Search by disjunction of local and remote attribute + res = self.ldb.search(expression="(|(revision=x)(lastLogon=y))", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 3) + res = sorted(res, key=attrgetter('dn')) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=B")) + self.assertFalse("dnsHostName" in res[0]) + self.assertEquals(str(res[0]["lastLogon"]), "y") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=X")) + self.assertEquals(str(res[1]["dnsHostName"]), "x") + self.assertEquals(str(res[1]["lastLogon"]), "x") + self.assertEquals(str(res[2].dn), self.samba4.dn("cn=Y")) + self.assertEquals(str(res[2]["dnsHostName"]), "y") + self.assertEquals(str(res[2]["lastLogon"]), "y") + + # Search by disjunction of local and remote attribute w/o match + res = self.ldb.search(expression="(|(codePage=y)(nextRid=z))", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 0) + + # Search by negated local attribute + res = self.ldb.search(expression="(!(revision=x))", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 6) + res = sorted(res, key=attrgetter('dn')) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=A")) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertEquals(str(res[0]["lastLogon"]), "x") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=B")) + self.assertTrue(not "dnsHostName" in res[1]) + self.assertEquals(str(res[1]["lastLogon"]), "y") + self.assertEquals(str(res[2].dn), self.samba4.dn("cn=C")) + self.assertTrue(not "dnsHostName" in res[2]) + self.assertEquals(str(res[2]["lastLogon"]), "z") + self.assertEquals(str(res[3].dn), self.samba4.dn("cn=Z")) + self.assertEquals(str(res[3]["dnsHostName"]), "z") + self.assertEquals(str(res[3]["lastLogon"]), "z") + + # Search by negated remote attribute + res = self.ldb.search(expression="(!(description=x))", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 4) + res = sorted(res, key=attrgetter('dn')) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=C")) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertEquals(str(res[0]["lastLogon"]), "z") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=Z")) + self.assertEquals(str(res[1]["dnsHostName"]), "z") + self.assertEquals(str(res[1]["lastLogon"]), "z") + + # Search by negated conjunction of local attributes + res = self.ldb.search(expression="(!(&(codePage=x)(revision=x)))", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 6) + res = sorted(res, key=attrgetter('dn')) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=A")) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertEquals(str(res[0]["lastLogon"]), "x") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=B")) + self.assertTrue(not "dnsHostName" in res[1]) + self.assertEquals(str(res[1]["lastLogon"]), "y") + self.assertEquals(str(res[2].dn), self.samba4.dn("cn=C")) + self.assertTrue(not "dnsHostName" in res[2]) + self.assertEquals(str(res[2]["lastLogon"]), "z") + self.assertEquals(str(res[3].dn), self.samba4.dn("cn=Z")) + self.assertEquals(str(res[3]["dnsHostName"]), "z") + self.assertEquals(str(res[3]["lastLogon"]), "z") + + # Search by negated conjunction of remote attributes + res = self.ldb.search(expression="(!(&(lastLogon=x)(description=x)))", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 6) + res = sorted(res, key=attrgetter('dn')) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=B")) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertEquals(str(res[0]["lastLogon"]), "y") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=C")) + self.assertTrue(not "dnsHostName" in res[1]) + self.assertEquals(str(res[1]["lastLogon"]), "z") + self.assertEquals(str(res[2].dn), self.samba4.dn("cn=Y")) + self.assertEquals(str(res[2]["dnsHostName"]), "y") + self.assertEquals(str(res[2]["lastLogon"]), "y") + self.assertEquals(str(res[3].dn), self.samba4.dn("cn=Z")) + self.assertEquals(str(res[3]["dnsHostName"]), "z") + self.assertEquals(str(res[3]["lastLogon"]), "z") + + # Search by negated conjunction of local and remote attribute + res = self.ldb.search(expression="(!(&(codePage=x)(description=x)))", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 6) + res = sorted(res, key=attrgetter('dn')) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=A")) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertEquals(str(res[0]["lastLogon"]), "x") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=B")) + self.assertTrue(not "dnsHostName" in res[1]) + self.assertEquals(str(res[1]["lastLogon"]), "y") + self.assertEquals(str(res[2].dn), self.samba4.dn("cn=C")) + self.assertTrue(not "dnsHostName" in res[2]) + self.assertEquals(str(res[2]["lastLogon"]), "z") + self.assertEquals(str(res[3].dn), self.samba4.dn("cn=Z")) + self.assertEquals(str(res[3]["dnsHostName"]), "z") + self.assertEquals(str(res[3]["lastLogon"]), "z") + + # Search by negated disjunction of local attributes + res = self.ldb.search(expression="(!(|(revision=x)(dnsHostName=x)))", + attrs=["dnsHostName", "lastLogon"]) + res = sorted(res, key=attrgetter('dn')) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=A")) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertEquals(str(res[0]["lastLogon"]), "x") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=B")) + self.assertTrue(not "dnsHostName" in res[1]) + self.assertEquals(str(res[1]["lastLogon"]), "y") + self.assertEquals(str(res[2].dn), self.samba4.dn("cn=C")) + self.assertTrue(not "dnsHostName" in res[2]) + self.assertEquals(str(res[2]["lastLogon"]), "z") + self.assertEquals(str(res[3].dn), self.samba4.dn("cn=Z")) + self.assertEquals(str(res[3]["dnsHostName"]), "z") + self.assertEquals(str(res[3]["lastLogon"]), "z") + + # Search by negated disjunction of remote attributes + res = self.ldb.search(expression="(!(|(badPwdCount=x)(lastLogon=x)))", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 5) + res = sorted(res, key=attrgetter('dn')) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=C")) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertEquals(str(res[0]["lastLogon"]), "z") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=Y")) + self.assertEquals(str(res[1]["dnsHostName"]), "y") + self.assertEquals(str(res[1]["lastLogon"]), "y") + self.assertEquals(str(res[2].dn), self.samba4.dn("cn=Z")) + self.assertEquals(str(res[2]["dnsHostName"]), "z") + self.assertEquals(str(res[2]["lastLogon"]), "z") + + # Search by negated disjunction of local and remote attribute + res = self.ldb.search(expression="(!(|(revision=x)(lastLogon=y)))", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 5) + res = sorted(res, key=attrgetter('dn')) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=A")) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertEquals(str(res[0]["lastLogon"]), "x") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=C")) + self.assertTrue(not "dnsHostName" in res[1]) + self.assertEquals(str(res[1]["lastLogon"]), "z") + self.assertEquals(str(res[2].dn), self.samba4.dn("cn=Z")) + self.assertEquals(str(res[2]["dnsHostName"]), "z") + self.assertEquals(str(res[2]["lastLogon"]), "z") + + # Search by complex parse tree + res = self.ldb.search(expression="(|(&(revision=x)(dnsHostName=x))(!(&(description=x)(nextRid=y)))(badPwdCount=y))", attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 7) + res = sorted(res, key=attrgetter('dn')) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=A")) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertEquals(str(res[0]["lastLogon"]), "x") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=B")) + self.assertTrue(not "dnsHostName" in res[1]) + self.assertEquals(str(res[1]["lastLogon"]), "y") + self.assertEquals(str(res[2].dn), self.samba4.dn("cn=C")) + self.assertTrue(not "dnsHostName" in res[2]) + self.assertEquals(str(res[2]["lastLogon"]), "z") + self.assertEquals(str(res[3].dn), self.samba4.dn("cn=X")) + self.assertEquals(str(res[3]["dnsHostName"]), "x") + self.assertEquals(str(res[3]["lastLogon"]), "x") + self.assertEquals(str(res[4].dn), self.samba4.dn("cn=Z")) + self.assertEquals(str(res[4]["dnsHostName"]), "z") + self.assertEquals(str(res[4]["lastLogon"]), "z") + + # Clean up + dns = [self.samba4.dn("cn=%s" % n) for n in ["A","B","C","X","Y","Z"]] + for dn in dns: + self.ldb.delete(dn) + + def test_map_modify_local(self): + """Modification of local records.""" + # Add local record + dn = "cn=test,dc=idealx,dc=org" + self.ldb.add({"dn": dn, + "cn": "test", + "foo": "bar", + "revision": "1", + "description": "test"}) + # Check it's there + attrs = ["foo", "revision", "description"] + res = self.ldb.search(dn, scope=SCOPE_BASE, attrs=attrs) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertEquals(str(res[0]["foo"]), "bar") + self.assertEquals(str(res[0]["revision"]), "1") + self.assertEquals(str(res[0]["description"]), "test") + # Check it's not in the local db + res = self.samba4.db.search(expression="(cn=test)", + scope=SCOPE_DEFAULT, attrs=attrs) + self.assertEquals(len(res), 0) + # Check it's not in the remote db + res = self.samba3.db.search(expression="(cn=test)", + scope=SCOPE_DEFAULT, attrs=attrs) + self.assertEquals(len(res), 0) + + # Modify local record + ldif = """ +dn: """ + dn + """ +replace: foo +foo: baz +replace: description +description: foo +""" + self.ldb.modify_ldif(ldif) + # Check in local db + res = self.ldb.search(dn, scope=SCOPE_BASE, attrs=attrs) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertEquals(str(res[0]["foo"]), "baz") + self.assertEquals(str(res[0]["revision"]), "1") + self.assertEquals(str(res[0]["description"]), "foo") + + # Rename local record + dn2 = "cn=toast,dc=idealx,dc=org" + self.ldb.rename(dn, dn2) + # Check in local db + res = self.ldb.search(dn2, scope=SCOPE_BASE, attrs=attrs) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn2) + self.assertEquals(str(res[0]["foo"]), "baz") + self.assertEquals(str(res[0]["revision"]), "1") + self.assertEquals(str(res[0]["description"]), "foo") + + # Delete local record + self.ldb.delete(dn2) + # Check it's gone + res = self.ldb.search(dn2, scope=SCOPE_BASE) + self.assertEquals(len(res), 0) + + def test_map_modify_remote_remote(self): + """Modification of remote data of remote records""" + # Add remote record + dn = self.samba4.dn("cn=test") + dn2 = self.samba3.dn("cn=test") + self.samba3.db.add({"dn": dn2, + "cn": "test", + "description": "foo", + "sambaBadPasswordCount": "3", + "sambaNextRid": "1001"}) + # Check it's there + res = self.samba3.db.search(dn2, scope=SCOPE_BASE, + attrs=["description", "sambaBadPasswordCount", "sambaNextRid"]) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn2) + self.assertEquals(str(res[0]["description"]), "foo") + self.assertEquals(str(res[0]["sambaBadPasswordCount"]), "3") + self.assertEquals(str(res[0]["sambaNextRid"]), "1001") + # Check in mapped db + attrs = ["description", "badPwdCount", "nextRid"] + res = self.ldb.search(dn, scope=SCOPE_BASE, attrs=attrs, expression="") + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertEquals(str(res[0]["description"]), "foo") + self.assertEquals(str(res[0]["badPwdCount"]), "3") + self.assertEquals(str(res[0]["nextRid"]), "1001") + # Check in local db + res = self.samba4.db.search(dn, scope=SCOPE_BASE, attrs=attrs) + self.assertEquals(len(res), 0) + + # Modify remote data of remote record + ldif = """ +dn: """ + dn + """ +replace: description +description: test +replace: badPwdCount +badPwdCount: 4 +""" + self.ldb.modify_ldif(ldif) + # Check in mapped db + res = self.ldb.search(dn, scope=SCOPE_BASE, + attrs=["description", "badPwdCount", "nextRid"]) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertEquals(str(res[0]["description"]), "test") + self.assertEquals(str(res[0]["badPwdCount"]), "4") + self.assertEquals(str(res[0]["nextRid"]), "1001") + # Check in remote db + res = self.samba3.db.search(dn2, scope=SCOPE_BASE, + attrs=["description", "sambaBadPasswordCount", "sambaNextRid"]) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn2) + self.assertEquals(str(res[0]["description"]), "test") + self.assertEquals(str(res[0]["sambaBadPasswordCount"]), "4") + self.assertEquals(str(res[0]["sambaNextRid"]), "1001") + + # Rename remote record + dn2 = self.samba4.dn("cn=toast") + self.ldb.rename(dn, dn2) + # Check in mapped db + dn = dn2 + res = self.ldb.search(dn, scope=SCOPE_BASE, + attrs=["description", "badPwdCount", "nextRid"]) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertEquals(str(res[0]["description"]), "test") + self.assertEquals(str(res[0]["badPwdCount"]), "4") + self.assertEquals(str(res[0]["nextRid"]), "1001") + # Check in remote db + dn2 = self.samba3.dn("cn=toast") + res = self.samba3.db.search(dn2, scope=SCOPE_BASE, + attrs=["description", "sambaBadPasswordCount", "sambaNextRid"]) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn2) + self.assertEquals(str(res[0]["description"]), "test") + self.assertEquals(str(res[0]["sambaBadPasswordCount"]), "4") + self.assertEquals(str(res[0]["sambaNextRid"]), "1001") + + # Delete remote record + self.ldb.delete(dn) + # Check in mapped db that it's removed + res = self.ldb.search(dn, scope=SCOPE_BASE) + self.assertEquals(len(res), 0) + # Check in remote db + res = self.samba3.db.search(dn2, scope=SCOPE_BASE) + self.assertEquals(len(res), 0) + + def test_map_modify_remote_local(self): + """Modification of local data of remote records""" + # Add remote record (same as before) + dn = self.samba4.dn("cn=test") + dn2 = self.samba3.dn("cn=test") + self.samba3.db.add({"dn": dn2, + "cn": "test", + "description": "foo", + "sambaBadPasswordCount": "3", + "sambaNextRid": "1001"}) + + # Modify local data of remote record + ldif = """ +dn: """ + dn + """ +add: revision +revision: 1 +replace: description +description: test + +""" + self.ldb.modify_ldif(ldif) + # Check in mapped db + attrs = ["revision", "description"] + res = self.ldb.search(dn, scope=SCOPE_BASE, attrs=attrs) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertEquals(str(res[0]["description"]), "test") + self.assertEquals(str(res[0]["revision"]), "1") + # Check in remote db + res = self.samba3.db.search(dn2, scope=SCOPE_BASE, attrs=attrs) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn2) + self.assertEquals(str(res[0]["description"]), "test") + self.assertTrue(not "revision" in res[0]) + # Check in local db + res = self.samba4.db.search(dn, scope=SCOPE_BASE, attrs=attrs) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertTrue(not "description" in res[0]) + self.assertEquals(str(res[0]["revision"]), "1") + + # Delete (newly) split record + self.ldb.delete(dn) + + def test_map_modify_split(self): + """Testing modification of split records""" + # Add split record + dn = self.samba4.dn("cn=test") + dn2 = self.samba3.dn("cn=test") + self.ldb.add({ + "dn": dn, + "cn": "test", + "description": "foo", + "badPwdCount": "3", + "nextRid": "1001", + "revision": "1"}) + # Check it's there + attrs = ["description", "badPwdCount", "nextRid", "revision"] + res = self.ldb.search(dn, scope=SCOPE_BASE, attrs=attrs) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertEquals(str(res[0]["description"]), "foo") + self.assertEquals(str(res[0]["badPwdCount"]), "3") + self.assertEquals(str(res[0]["nextRid"]), "1001") + self.assertEquals(str(res[0]["revision"]), "1") + # Check in local db + res = self.samba4.db.search(dn, scope=SCOPE_BASE, attrs=attrs) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertTrue(not "description" in res[0]) + self.assertTrue(not "badPwdCount" in res[0]) + self.assertTrue(not "nextRid" in res[0]) + self.assertEquals(str(res[0]["revision"]), "1") + # Check in remote db + attrs = ["description", "sambaBadPasswordCount", "sambaNextRid", + "revision"] + res = self.samba3.db.search(dn2, scope=SCOPE_BASE, attrs=attrs) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn2) + self.assertEquals(str(res[0]["description"]), "foo") + self.assertEquals(str(res[0]["sambaBadPasswordCount"]), "3") + self.assertEquals(str(res[0]["sambaNextRid"]), "1001") + self.assertTrue(not "revision" in res[0]) + + # Modify of split record + ldif = """ +dn: """ + dn + """ +replace: description +description: test +replace: badPwdCount +badPwdCount: 4 +replace: revision +revision: 2 +""" + self.ldb.modify_ldif(ldif) + # Check in mapped db + attrs = ["description", "badPwdCount", "nextRid", "revision"] + res = self.ldb.search(dn, scope=SCOPE_BASE, attrs=attrs) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertEquals(str(res[0]["description"]), "test") + self.assertEquals(str(res[0]["badPwdCount"]), "4") + self.assertEquals(str(res[0]["nextRid"]), "1001") + self.assertEquals(str(res[0]["revision"]), "2") + # Check in local db + res = self.samba4.db.search(dn, scope=SCOPE_BASE, attrs=attrs) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertTrue(not "description" in res[0]) + self.assertTrue(not "badPwdCount" in res[0]) + self.assertTrue(not "nextRid" in res[0]) + self.assertEquals(str(res[0]["revision"]), "2") + # Check in remote db + attrs = ["description", "sambaBadPasswordCount", "sambaNextRid", + "revision"] + res = self.samba3.db.search(dn2, scope=SCOPE_BASE, attrs=attrs) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn2) + self.assertEquals(str(res[0]["description"]), "test") + self.assertEquals(str(res[0]["sambaBadPasswordCount"]), "4") + self.assertEquals(str(res[0]["sambaNextRid"]), "1001") + self.assertTrue(not "revision" in res[0]) + + # Rename split record + dn2 = self.samba4.dn("cn=toast") + self.ldb.rename(dn, dn2) + # Check in mapped db + dn = dn2 + attrs = ["description", "badPwdCount", "nextRid", "revision"] + res = self.ldb.search(dn, scope=SCOPE_BASE, attrs=attrs) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertEquals(str(res[0]["description"]), "test") + self.assertEquals(str(res[0]["badPwdCount"]), "4") + self.assertEquals(str(res[0]["nextRid"]), "1001") + self.assertEquals(str(res[0]["revision"]), "2") + # Check in local db + res = self.samba4.db.search(dn, scope=SCOPE_BASE, attrs=attrs) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertTrue(not "description" in res[0]) + self.assertTrue(not "badPwdCount" in res[0]) + self.assertTrue(not "nextRid" in res[0]) + self.assertEquals(str(res[0]["revision"]), "2") + # Check in remote db + dn2 = self.samba3.dn("cn=toast") + res = self.samba3.db.search(dn2, scope=SCOPE_BASE, + attrs=["description", "sambaBadPasswordCount", "sambaNextRid", + "revision"]) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn2) + self.assertEquals(str(res[0]["description"]), "test") + self.assertEquals(str(res[0]["sambaBadPasswordCount"]), "4") + self.assertEquals(str(res[0]["sambaNextRid"]), "1001") + self.assertTrue(not "revision" in res[0]) + + # Delete split record + self.ldb.delete(dn) + # Check in mapped db + res = self.ldb.search(dn, scope=SCOPE_BASE) + self.assertEquals(len(res), 0) + # Check in local db + res = self.samba4.db.search(dn, scope=SCOPE_BASE) + self.assertEquals(len(res), 0) + # Check in remote db + res = self.samba3.db.search(dn2, scope=SCOPE_BASE) + self.assertEquals(len(res), 0) diff --git a/python/samba/tests/samba_tool/__init__.py b/python/samba/tests/samba_tool/__init__.py new file mode 100644 index 00000000000..3d7f0591e25 --- /dev/null +++ b/python/samba/tests/samba_tool/__init__.py @@ -0,0 +1,15 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Sean Dague <sdague@linux.vnet.ibm.com +# +# 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/>. diff --git a/python/samba/tests/samba_tool/base.py b/python/samba/tests/samba_tool/base.py new file mode 100644 index 00000000000..60ccaa543d7 --- /dev/null +++ b/python/samba/tests/samba_tool/base.py @@ -0,0 +1,114 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Sean Dague <sdague@linux.vnet.ibm.com> 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/>. + +# This provides a wrapper around the cmd interface so that tests can +# easily be built on top of it and have minimal code to run basic tests +# of the commands. A list of the environmental variables can be found in +# ~/selftest/selftest.pl +# +# These can all be accesses via os.environ["VARIBLENAME"] when needed + +import random +import string +from samba.auth import system_session +from samba.samdb import SamDB +from cStringIO import StringIO +from samba.netcmd.main import cmd_sambatool +import samba.tests + +class SambaToolCmdTest(samba.tests.TestCaseInTempDir): + + def getSamDB(self, *argv): + """a convenience function to get a samdb instance so that we can query it""" + + # We build a fake command to get the options created the same + # way the command classes do it. It would be better if the command + # classes had a way to more cleanly do this, but this lets us write + # tests for now + cmd = cmd_sambatool.subcommands["user"].subcommands["setexpiry"] + parser, optiongroups = cmd._create_parser("user") + opts, args = parser.parse_args(list(argv)) + # Filter out options from option groups + args = args[1:] + kwargs = dict(opts.__dict__) + for option_group in parser.option_groups: + for option in option_group.option_list: + if option.dest is not None: + del kwargs[option.dest] + kwargs.update(optiongroups) + + H = kwargs.get("H", None) + sambaopts = kwargs.get("sambaopts", None) + credopts = kwargs.get("credopts", None) + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + return samdb + + + def runcmd(self, name, *args): + """run a single level command""" + cmd = cmd_sambatool.subcommands[name] + cmd.outf = StringIO() + cmd.errf = StringIO() + result = cmd._run(name, *args) + return (result, cmd.outf.getvalue(), cmd.errf.getvalue()) + + def runsubcmd(self, name, sub, *args): + """run a command with sub commands""" + # The reason we need this function seperate from runcmd is + # that the .outf StringIO assignment is overriden if we use + # runcmd, so we can't capture stdout and stderr + cmd = cmd_sambatool.subcommands[name].subcommands[sub] + cmd.outf = StringIO() + cmd.errf = StringIO() + result = cmd._run(name, *args) + return (result, cmd.outf.getvalue(), cmd.errf.getvalue()) + + def assertCmdSuccess(self, val, msg=""): + self.assertIsNone(val, msg) + + def assertCmdFail(self, val, msg=""): + self.assertIsNotNone(val, msg) + + def assertMatch(self, base, string, msg=""): + self.assertTrue(string in base, msg) + + def randomName(self, count=8): + """Create a random name, cap letters and numbers, and always starting with a letter""" + name = random.choice(string.ascii_uppercase) + name += ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase+ string.digits) for x in range(count - 1)) + return name + + def randomPass(self, count=16): + name = random.choice(string.ascii_uppercase) + name += random.choice(string.digits) + name += random.choice(string.ascii_lowercase) + name += ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase+ string.digits) for x in range(count - 3)) + return name + + def randomXid(self): + # pick some hopefully unused, high UID/GID range to avoid interference + # from the system the test runs on + xid = random.randint(4711000, 4799000) + return xid + + def assertWithin(self, val1, val2, delta, msg=""): + """Assert that val1 is within delta of val2, useful for time computations""" + self.assertTrue(((val1 + delta) > val2) and ((val1 - delta) < val2), msg) diff --git a/python/samba/tests/samba_tool/gpo.py b/python/samba/tests/samba_tool/gpo.py new file mode 100644 index 00000000000..e20a97794ae --- /dev/null +++ b/python/samba/tests/samba_tool/gpo.py @@ -0,0 +1,79 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Andrew Bartlett 2012 +# +# based on time.py: +# Copyright (C) Sean Dague <sdague@linux.vnet.ibm.com> 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 os +from samba.tests.samba_tool.base import SambaToolCmdTest +import shutil + +class GpoCmdTestCase(SambaToolCmdTest): + """Tests for samba-tool time subcommands""" + + gpo_name = "testgpo" + + def test_gpo_list(self): + """Run gpo list against the server and make sure it looks accurate""" + (result, out, err) = self.runsubcmd("gpo", "listall", "-H", "ldap://%s" % os.environ["SERVER"]) + self.assertCmdSuccess(result, "Ensuring gpo listall ran successfully") + + def test_fetchfail(self): + """Run against a non-existent GPO, and make sure it fails (this hard-coded UUID is very unlikely to exist""" + (result, out, err) = self.runsubcmd("gpo", "fetch", "c25cac17-a02a-4151-835d-fae17446ee43", "-H", "ldap://%s" % os.environ["SERVER"]) + self.assertEquals(result, -1, "check for result code") + + def test_fetch(self): + """Run against a real GPO, and make sure it passes""" + (result, out, err) = self.runsubcmd("gpo", "fetch", self.gpo_guid, "-H", "ldap://%s" % os.environ["SERVER"], "--tmpdir", self.tempdir) + self.assertCmdSuccess(result, "Ensuring gpo fetched successfully") + shutil.rmtree(os.path.join(self.tempdir, "policy")) + + def test_show(self): + """Show a real GPO, and make sure it passes""" + (result, out, err) = self.runsubcmd("gpo", "show", self.gpo_guid, "-H", "ldap://%s" % os.environ["SERVER"]) + self.assertCmdSuccess(result, "Ensuring gpo fetched successfully") + + def test_show_as_admin(self): + """Show a real GPO, and make sure it passes""" + (result, out, err) = self.runsubcmd("gpo", "show", self.gpo_guid, "-H", "ldap://%s" % os.environ["SERVER"], "-U%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])) + self.assertCmdSuccess(result, "Ensuring gpo fetched successfully") + + def test_aclcheck(self): + """Check all the GPOs on the remote server have correct ACLs""" + (result, out, err) = self.runsubcmd("gpo", "aclcheck", "-H", "ldap://%s" % os.environ["SERVER"], "-U%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])) + self.assertCmdSuccess(result, "Ensuring gpo checked successfully") + + def setUp(self): + """set up a temporary GPO to work with""" + super(GpoCmdTestCase, self).setUp() + (result, out, err) = self.runsubcmd("gpo", "create", self.gpo_name, + "-H", "ldap://%s" % os.environ["SERVER"], + "-U%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"]), + "--tmpdir", self.tempdir) + shutil.rmtree(os.path.join(self.tempdir, "policy")) + self.assertCmdSuccess(result, "Ensuring gpo created successfully") + try: + self.gpo_guid = "{%s}" % out.split("{")[1].split("}")[0] + except IndexError: + self.fail("Failed to find GUID in output: %s" % out) + + def tearDown(self): + """remove the temporary GPO to work with""" + (result, out, err) = self.runsubcmd("gpo", "del", self.gpo_guid, "-H", "ldap://%s" % os.environ["SERVER"], "-U%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])) + self.assertCmdSuccess(result, "Ensuring gpo deleted successfully") + super(GpoCmdTestCase, self).tearDown() diff --git a/python/samba/tests/samba_tool/group.py b/python/samba/tests/samba_tool/group.py new file mode 100644 index 00000000000..2c0c46e5dc8 --- /dev/null +++ b/python/samba/tests/samba_tool/group.py @@ -0,0 +1,169 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Michael Adam 2012 +# +# 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 os +import time +import ldb +from samba.tests.samba_tool.base import SambaToolCmdTest +from samba import ( + nttime2unix, + dsdb + ) + +class GroupCmdTestCase(SambaToolCmdTest): + """Tests for samba-tool group subcommands""" + groups = [] + samdb = None + + def setUp(self): + super(GroupCmdTestCase, self).setUp() + self.samdb = self.getSamDB("-H", "ldap://%s" % os.environ["DC_SERVER"], + "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"])) + self.groups = [] + self.groups.append(self._randomGroup({"name": "testgroup1"})) + self.groups.append(self._randomGroup({"name": "testgroup2"})) + self.groups.append(self._randomGroup({"name": "testgroup3"})) + self.groups.append(self._randomGroup({"name": "testgroup4"})) + + # setup the 4 groups and ensure they are correct + for group in self.groups: + (result, out, err) = self._create_group(group) + + self.assertCmdSuccess(result) + self.assertEquals(err, "", "There shouldn't be any error message") + self.assertIn("Added group %s" % group["name"], out) + + found = self._find_group(group["name"]) + + self.assertIsNotNone(found) + + self.assertEquals("%s" % found.get("name"), group["name"]) + self.assertEquals("%s" % found.get("description"), group["description"]) + + def tearDown(self): + super(GroupCmdTestCase, self).tearDown() + # clean up all the left over groups, just in case + for group in self.groups: + if self._find_group(group["name"]): + self.runsubcmd("group", "delete", group["name"]) + + + def test_newgroup(self): + """This tests the "group add" and "group delete" commands""" + # try to add all the groups again, this should fail + for group in self.groups: + (result, out, err) = self._create_group(group) + self.assertCmdFail(result, "Succeeded to create existing group") + self.assertIn("LDAP error 68 LDAP_ENTRY_ALREADY_EXISTS", err) + + # try to delete all the groups we just added + for group in self.groups: + (result, out, err) = self.runsubcmd("group", "delete", group["name"]) + self.assertCmdSuccess(result, + "Failed to delete group '%s'" % group["name"]) + found = self._find_group(group["name"]) + self.assertIsNone(found, + "Deleted group '%s' still exists" % group["name"]) + + # test adding groups + for group in self.groups: + (result, out, err) = self.runsubcmd("group", "add", group["name"], + "--description=%s" % group["description"], + "-H", "ldap://%s" % os.environ["DC_SERVER"], + "-U%s%%%s" % (os.environ["DC_USERNAME"], + os.environ["DC_PASSWORD"])) + + self.assertCmdSuccess(result) + self.assertEquals(err,"","There shouldn't be any error message") + self.assertIn("Added group %s" % group["name"], out) + + found = self._find_group(group["name"]) + + self.assertEquals("%s" % found.get("samaccountname"), + "%s" % group["name"]) + + + def test_list(self): + (result, out, err) = self.runsubcmd("group", "list", + "-H", "ldap://%s" % os.environ["DC_SERVER"], + "-U%s%%%s" % (os.environ["DC_USERNAME"], + os.environ["DC_PASSWORD"])) + self.assertCmdSuccess(result, "Error running list") + + search_filter = "(objectClass=group)" + + grouplist = self.samdb.search(base=self.samdb.domain_dn(), + scope=ldb.SCOPE_SUBTREE, + expression=search_filter, + attrs=["samaccountname"]) + + self.assertTrue(len(grouplist) > 0, "no groups found in samdb") + + for groupobj in grouplist: + name = groupobj.get("samaccountname", idx=0) + found = self.assertMatch(out, name, + "group '%s' not found" % name) + + def test_listmembers(self): + (result, out, err) = self.runsubcmd("group", "listmembers", "Domain Users", + "-H", "ldap://%s" % os.environ["DC_SERVER"], + "-U%s%%%s" % (os.environ["DC_USERNAME"], + os.environ["DC_PASSWORD"])) + self.assertCmdSuccess(result, "Error running listmembers") + + search_filter = "(|(primaryGroupID=513)(memberOf=CN=Domain Users,CN=Users,%s))" % self.samdb.domain_dn() + + grouplist = self.samdb.search(base=self.samdb.domain_dn(), + scope=ldb.SCOPE_SUBTREE, + expression=search_filter, + attrs=["samAccountName"]) + + self.assertTrue(len(grouplist) > 0, "no groups found in samdb") + + for groupobj in grouplist: + name = groupobj.get("samAccountName", idx=0) + found = self.assertMatch(out, name, "group '%s' not found" % name) + + def _randomGroup(self, base={}): + """create a group with random attribute values, you can specify base attributes""" + group = { + "name": self.randomName(), + "description": self.randomName(count=100), + } + group.update(base) + return group + + def _create_group(self, group): + return self.runsubcmd("group", "add", group["name"], + "--description=%s" % group["description"], + "-H", "ldap://%s" % os.environ["DC_SERVER"], + "-U%s%%%s" % (os.environ["DC_USERNAME"], + os.environ["DC_PASSWORD"])) + + def _find_group(self, name): + search_filter = ("(&(sAMAccountName=%s)(objectCategory=%s,%s))" % + (ldb.binary_encode(name), + "CN=Group,CN=Schema,CN=Configuration", + self.samdb.domain_dn())) + grouplist = self.samdb.search(base=self.samdb.domain_dn(), + scope=ldb.SCOPE_SUBTREE, + expression=search_filter, + attrs=[]) + if grouplist: + return grouplist[0] + else: + return None diff --git a/python/samba/tests/samba_tool/ntacl.py b/python/samba/tests/samba_tool/ntacl.py new file mode 100644 index 00000000000..2a329fe7d40 --- /dev/null +++ b/python/samba/tests/samba_tool/ntacl.py @@ -0,0 +1,135 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Andrew Bartlett 2012 +# +# Based on user.py: +# Copyright (C) Sean Dague <sdague@linux.vnet.ibm.com> 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 os +import time +import ldb +from samba.tests.samba_tool.base import SambaToolCmdTest +import random + +class NtACLCmdSysvolTestCase(SambaToolCmdTest): + """Tests for samba-tool ntacl sysvol* subcommands""" + + + def test_ntvfs(self): + (result, out, err) = self.runsubcmd("ntacl", "sysvolreset", + "--use-ntvfs") + self.assertCmdSuccess(result) + self.assertEquals(out,"","Shouldn't be any output messages") + self.assertIn("Please note that POSIX permissions have NOT been changed, only the stored NT ACL", err) + + def test_s3fs(self): + (result, out, err) = self.runsubcmd("ntacl", "sysvolreset", + "--use-s3fs") + + self.assertCmdSuccess(result) + self.assertEquals(err,"","Shouldn't be any error messages") + self.assertEquals(out,"","Shouldn't be any output messages") + + def test_ntvfs_check(self): + (result, out, err) = self.runsubcmd("ntacl", "sysvolreset", + "--use-ntvfs") + self.assertCmdSuccess(result) + self.assertEquals(out,"","Shouldn't be any output messages") + self.assertIn("Please note that POSIX permissions have NOT been changed, only the stored NT ACL", err) + # Now check they were set correctly + (result, out, err) = self.runsubcmd("ntacl", "sysvolcheck") + self.assertCmdSuccess(result) + self.assertEquals(err,"","Shouldn't be any error messages") + self.assertEquals(out,"","Shouldn't be any output messages") + + def test_s3fs_check(self): + (result, out, err) = self.runsubcmd("ntacl", "sysvolreset", + "--use-s3fs") + + self.assertCmdSuccess(result) + self.assertEquals(err,"","Shouldn't be any error messages") + self.assertEquals(out,"","Shouldn't be any output messages") + + # Now check they were set correctly + (result, out, err) = self.runsubcmd("ntacl", "sysvolcheck") + self.assertCmdSuccess(result) + self.assertEquals(err,"","Shouldn't be any error messages") + self.assertEquals(out,"","Shouldn't be any output messages") + +class NtACLCmdGetSetTestCase(SambaToolCmdTest): + """Tests for samba-tool ntacl get/set subcommands""" + + acl = "O:DAG:DUD:P(A;OICI;0x001f01ff;;;DA)(A;OICI;0x001f01ff;;;EA)(A;OICIIO;0x001f01ff;;;CO)(A;OICI;0x001f01ff;;;DA)(A;OICI;0x001f01ff;;;SY)(A;OICI;0x001200a9;;;AU)(A;OICI;0x001200a9;;;ED)S:AI(OU;CIIDSA;WP;f30e3bbe-9ff0-11d1-b603-0000f80367c1;bf967aa5-0de6-11d0-a285-00aa003049e2;WD)(OU;CIIDSA;WP;f30e3bbf-9ff0-11d1-b603-0000f80367c1;bf967aa5-0de6-11d0-a285-00aa003049e2;WD)" + + + def test_ntvfs(self): + path = os.environ['SELFTEST_PREFIX'] + tempf = os.path.join(path,"pytests"+str(int(100000*random.random()))) + open(tempf, 'w').write("empty") + + (result, out, err) = self.runsubcmd("ntacl", "set", self.acl, tempf, + "--use-ntvfs") + self.assertCmdSuccess(result) + self.assertEquals(out,"","Shouldn't be any output messages") + self.assertIn("Please note that POSIX permissions have NOT been changed, only the stored NT ACL", err) + + def test_s3fs(self): + path = os.environ['SELFTEST_PREFIX'] + tempf = os.path.join(path,"pytests"+str(int(100000*random.random()))) + open(tempf, 'w').write("empty") + + (result, out, err) = self.runsubcmd("ntacl", "set", self.acl, tempf, + "--use-s3fs") + + self.assertCmdSuccess(result) + self.assertEquals(err,"","Shouldn't be any error messages") + self.assertEquals(out,"","Shouldn't be any output messages") + + def test_ntvfs_check(self): + path = os.environ['SELFTEST_PREFIX'] + tempf = os.path.join(path,"pytests"+str(int(100000*random.random()))) + open(tempf, 'w').write("empty") + + (result, out, err) = self.runsubcmd("ntacl", "set", self.acl, tempf, + "--use-ntvfs") + self.assertCmdSuccess(result) + self.assertEquals(out,"","Shouldn't be any output messages") + self.assertIn("Please note that POSIX permissions have NOT been changed, only the stored NT ACL", err) + + # Now check they were set correctly + (result, out, err) = self.runsubcmd("ntacl", "get", tempf, + "--use-ntvfs", "--as-sddl") + self.assertCmdSuccess(result) + self.assertEquals(err,"","Shouldn't be any error messages") + self.assertEquals(self.acl+"\n", out, "Output should be the ACL") + + def test_s3fs_check(self): + path = os.environ['SELFTEST_PREFIX'] + tempf = os.path.join(path,"pytests"+str(int(100000*random.random()))) + open(tempf, 'w').write("empty") + + (result, out, err) = self.runsubcmd("ntacl", "set", self.acl, tempf, + "--use-s3fs") + self.assertCmdSuccess(result) + self.assertEquals(out,"","Shouldn't be any output messages") + self.assertEquals(err,"","Shouldn't be any error messages") + + # Now check they were set correctly + (result, out, err) = self.runsubcmd("ntacl", "get", tempf, + "--use-s3fs", "--as-sddl") + self.assertCmdSuccess(result) + self.assertEquals(err,"","Shouldn't be any error messages") + self.assertEquals(self.acl+"\n", out,"Output should be the ACL") diff --git a/python/samba/tests/samba_tool/processes.py b/python/samba/tests/samba_tool/processes.py new file mode 100644 index 00000000000..91a5266b54a --- /dev/null +++ b/python/samba/tests/samba_tool/processes.py @@ -0,0 +1,35 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Andrew Bartlett 2012 +# +# based on time.py: +# Copyright (C) Sean Dague <sdague@linux.vnet.ibm.com> 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 os +from samba.tests.samba_tool.base import SambaToolCmdTest + +class ProcessCmdTestCase(SambaToolCmdTest): + """Tests for samba-tool process subcommands""" + + def test_name(self): + """Run processes command""" + (result, out, err) = self.runcmd("processes", "--name", "samba") + self.assertCmdSuccess(result, "Ensuring processes ran successfully") + + def test_all(self): + """Run processes command""" + (result, out, err) = self.runcmd("processes") + self.assertCmdSuccess(result, "Ensuring processes ran successfully") diff --git a/python/samba/tests/samba_tool/timecmd.py b/python/samba/tests/samba_tool/timecmd.py new file mode 100644 index 00000000000..000f0f28282 --- /dev/null +++ b/python/samba/tests/samba_tool/timecmd.py @@ -0,0 +1,43 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Sean Dague <sdague@linux.vnet.ibm.com> 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 os +from time import localtime, strptime, mktime +from samba.tests.samba_tool.base import SambaToolCmdTest + +class TimeCmdTestCase(SambaToolCmdTest): + """Tests for samba-tool time subcommands""" + + def test_timeget(self): + """Run time against the server and make sure it looks accurate""" + (result, out, err) = self.runcmd("time", os.environ["SERVER"]) + self.assertCmdSuccess(result, "Ensuring time ran successfully") + + timefmt = strptime(out, "%a %b %d %H:%M:%S %Y %Z\n") + servertime = int(mktime(timefmt)) + now = int(mktime(localtime())) + + # because there is a race here, allow up to 5 seconds difference in times + delta = 5 + self.assertTrue((servertime > (now - delta) and (servertime < (now + delta)), "Time is now")) + + def test_timefail(self): + """Run time against a non-existent server, and make sure it fails""" + (result, out, err) = self.runcmd("time", "notaserver") + self.assertEquals(result, -1, "check for result code") + self.assertTrue(err.strip().endswith("NT_STATUS_OBJECT_NAME_NOT_FOUND"), "ensure right error string") + self.assertEquals(out, "", "ensure no output returned") diff --git a/python/samba/tests/samba_tool/user.py b/python/samba/tests/samba_tool/user.py new file mode 100644 index 00000000000..33344cd3d31 --- /dev/null +++ b/python/samba/tests/samba_tool/user.py @@ -0,0 +1,362 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Sean Dague <sdague@linux.vnet.ibm.com> 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 os +import time +import ldb +from samba.tests.samba_tool.base import SambaToolCmdTest +from samba import ( + nttime2unix, + dsdb + ) + +class UserCmdTestCase(SambaToolCmdTest): + """Tests for samba-tool user subcommands""" + users = [] + samdb = None + + def setUp(self): + super(UserCmdTestCase, self).setUp() + self.samdb = self.getSamDB("-H", "ldap://%s" % os.environ["DC_SERVER"], + "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"])) + self.users = [] + self.users.append(self._randomUser({"name": "sambatool1", "company": "comp1"})) + self.users.append(self._randomUser({"name": "sambatool2", "company": "comp1"})) + self.users.append(self._randomUser({"name": "sambatool3", "company": "comp2"})) + self.users.append(self._randomUser({"name": "sambatool4", "company": "comp2"})) + self.users.append(self._randomPosixUser({"name": "posixuser1"})) + self.users.append(self._randomPosixUser({"name": "posixuser2"})) + self.users.append(self._randomPosixUser({"name": "posixuser3"})) + self.users.append(self._randomPosixUser({"name": "posixuser4"})) + + # setup the 8 users and ensure they are correct + for user in self.users: + (result, out, err) = user["createUserFn"](user) + + self.assertCmdSuccess(result) + self.assertEquals(err,"","Shouldn't be any error messages") + self.assertIn("User '%s' created successfully" % user["name"], out) + + user["checkUserFn"](user) + + + def tearDown(self): + super(UserCmdTestCase, self).tearDown() + # clean up all the left over users, just in case + for user in self.users: + if self._find_user(user["name"]): + self.runsubcmd("user", "delete", user["name"]) + + + def test_newuser(self): + # try to add all the users again, this should fail + for user in self.users: + (result, out, err) = self._create_user(user) + self.assertCmdFail(result, "Ensure that create user fails") + self.assertIn("LDAP error 68 LDAP_ENTRY_ALREADY_EXISTS", err) + + # try to delete all the 4 users we just added + for user in self.users: + (result, out, err) = self.runsubcmd("user", "delete", user["name"]) + self.assertCmdSuccess(result, "Can we delete users") + found = self._find_user(user["name"]) + self.assertIsNone(found) + + # test adding users with --use-username-as-cn + for user in self.users: + (result, out, err) = self.runsubcmd("user", "add", user["name"], user["password"], + "--use-username-as-cn", + "--surname=%s" % user["surname"], + "--given-name=%s" % user["given-name"], + "--job-title=%s" % user["job-title"], + "--department=%s" % user["department"], + "--description=%s" % user["description"], + "--company=%s" % user["company"], + "-H", "ldap://%s" % os.environ["DC_SERVER"], + "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"])) + + self.assertCmdSuccess(result) + self.assertEquals(err,"","Shouldn't be any error messages") + self.assertIn("User '%s' created successfully" % user["name"], out) + + found = self._find_user(user["name"]) + + self.assertEquals("%s" % found.get("cn"), "%(name)s" % user) + self.assertEquals("%s" % found.get("name"), "%(name)s" % user) + + + + def test_setpassword(self): + for user in self.users: + newpasswd = self.randomPass() + (result, out, err) = self.runsubcmd("user", "setpassword", + user["name"], + "--newpassword=%s" % newpasswd, + "-H", "ldap://%s" % os.environ["DC_SERVER"], + "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"])) + # self.assertCmdSuccess(result, "Ensure setpassword runs") + self.assertEquals(err,"","setpassword with url") + self.assertMatch(out, "Changed password OK", "setpassword with url") + + for user in self.users: + newpasswd = self.randomPass() + (result, out, err) = self.runsubcmd("user", "setpassword", + user["name"], + "--newpassword=%s" % newpasswd) + # self.assertCmdSuccess(result, "Ensure setpassword runs") + self.assertEquals(err,"","setpassword without url") + self.assertMatch(out, "Changed password OK", "setpassword without url") + + for user in self.users: + newpasswd = self.randomPass() + (result, out, err) = self.runsubcmd("user", "setpassword", + user["name"], + "--newpassword=%s" % newpasswd, + "--must-change-at-next-login", + "-H", "ldap://%s" % os.environ["DC_SERVER"], + "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"])) + # self.assertCmdSuccess(result, "Ensure setpassword runs") + self.assertEquals(err,"","setpassword with forced change") + self.assertMatch(out, "Changed password OK", "setpassword with forced change") + + + + + def test_setexpiry(self): + twodays = time.time() + (2 * 24 * 60 * 60) + + for user in self.users: + (result, out, err) = self.runsubcmd("user", "setexpiry", user["name"], + "--days=2", + "-H", "ldap://%s" % os.environ["DC_SERVER"], + "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"])) + self.assertCmdSuccess(result, "Can we run setexpiry with names") + self.assertIn("Expiry for user '%s' set to 2 days." % user["name"], out) + + for user in self.users: + found = self._find_user(user["name"]) + + expires = nttime2unix(int("%s" % found.get("accountExpires"))) + self.assertWithin(expires, twodays, 5, "Ensure account expires is within 5 seconds of the expected time") + + # TODO: renable this after the filter case is sorted out + if "filters are broken, bail now": + return + + # now run the expiration based on a filter + fourdays = time.time() + (4 * 24 * 60 * 60) + (result, out, err) = self.runsubcmd("user", "setexpiry", + "--filter", "(&(objectClass=user)(company=comp2))", + "--days=4", + "-H", "ldap://%s" % os.environ["DC_SERVER"], + "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"])) + self.assertCmdSuccess(result, "Can we run setexpiry with a filter") + + for user in self.users: + found = self._find_user(user["name"]) + if ("%s" % found.get("company")) == "comp2": + expires = nttime2unix(int("%s" % found.get("accountExpires"))) + self.assertWithin(expires, fourdays, 5, "Ensure account expires is within 5 seconds of the expected time") + else: + expires = nttime2unix(int("%s" % found.get("accountExpires"))) + self.assertWithin(expires, twodays, 5, "Ensure account expires is within 5 seconds of the expected time") + + + def test_list(self): + (result, out, err) = self.runsubcmd("user", "list", + "-H", "ldap://%s" % os.environ["DC_SERVER"], + "-U%s%%%s" % (os.environ["DC_USERNAME"], + os.environ["DC_PASSWORD"])) + self.assertCmdSuccess(result, "Error running list") + + search_filter = ("(&(objectClass=user)(userAccountControl:%s:=%u))" % + (ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT)) + + userlist = self.samdb.search(base=self.samdb.domain_dn(), + scope=ldb.SCOPE_SUBTREE, + expression=search_filter, + attrs=["samaccountname"]) + + self.assertTrue(len(userlist) > 0, "no users found in samdb") + + for userobj in userlist: + name = userobj.get("samaccountname", idx=0) + found = self.assertMatch(out, name, + "user '%s' not found" % name) + def test_getpwent(self): + try: + import pwd + except ImportError: + self.skipTest("Skipping getpwent test, no 'pwd' module available") + return + + # get the current user's data for the test + uid = os.geteuid() + try: + u = pwd.getpwuid(uid) + except KeyError: + self.skipTest("Skipping getpwent test, current EUID not found in NSS") + return + + user = self._randomPosixUser({ + "name": u[0], + "uid": u[0], + "uidNumber": u[2], + "gidNumber": u[3], + "gecos": u[4], + "loginShell": u[6], + }) + # check if --rfc2307-from-nss sets the same values as we got from pwd.getpwuid() + (result, out, err) = self.runsubcmd("user", "add", user["name"], user["password"], + "--surname=%s" % user["surname"], + "--given-name=%s" % user["given-name"], + "--job-title=%s" % user["job-title"], + "--department=%s" % user["department"], + "--description=%s" % user["description"], + "--company=%s" % user["company"], + "--rfc2307-from-nss", + "-H", "ldap://%s" % os.environ["DC_SERVER"], + "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"])) + + self.assertCmdSuccess(result) + self.assertEquals(err,"","Shouldn't be any error messages") + self.assertIn("User '%s' created successfully" % user["name"], out) + + self._check_posix_user(user) + self.runsubcmd("user", "delete", user["name"]) + + # Check if overriding the attributes from NSS with explicit values works + # + # get a user with all random posix attributes + user = self._randomPosixUser({"name": u[0]}) + # create a user with posix attributes from nss but override all of them with the + # random ones just obtained + (result, out, err) = self.runsubcmd("user", "add", user["name"], user["password"], + "--surname=%s" % user["surname"], + "--given-name=%s" % user["given-name"], + "--job-title=%s" % user["job-title"], + "--department=%s" % user["department"], + "--description=%s" % user["description"], + "--company=%s" % user["company"], + "--rfc2307-from-nss", + "--gecos=%s" % user["gecos"], + "--login-shell=%s" % user["loginShell"], + "--uid=%s" % user["uid"], + "--uid-number=%s" % user["uidNumber"], + "--gid-number=%s" % user["gidNumber"], + "-H", "ldap://%s" % os.environ["DC_SERVER"], + "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"])) + + self.assertCmdSuccess(result) + self.assertEquals(err,"","Shouldn't be any error messages") + self.assertIn("User '%s' created successfully" % user["name"], out) + + self._check_posix_user(user) + self.runsubcmd("user", "delete", user["name"]) + + def _randomUser(self, base={}): + """create a user with random attribute values, you can specify base attributes""" + user = { + "name": self.randomName(), + "password": self.randomPass(), + "surname": self.randomName(), + "given-name": self.randomName(), + "job-title": self.randomName(), + "department": self.randomName(), + "company": self.randomName(), + "description": self.randomName(count=100), + "createUserFn": self._create_user, + "checkUserFn": self._check_user, + } + user.update(base) + return user + + def _randomPosixUser(self, base={}): + """create a user with random attribute values and additional RFC2307 + attributes, you can specify base attributes""" + user = self._randomUser({}) + user.update(base) + posixAttributes = { + "uid": self.randomName(), + "loginShell": self.randomName(), + "gecos": self.randomName(), + "uidNumber": self.randomXid(), + "gidNumber": self.randomXid(), + "createUserFn": self._create_posix_user, + "checkUserFn": self._check_posix_user, + } + user.update(posixAttributes) + user.update(base) + return user + + def _check_user(self, user): + """ check if a user from SamDB has the same attributes as its template """ + found = self._find_user(user["name"]) + + self.assertEquals("%s" % found.get("name"), "%(given-name)s %(surname)s" % user) + self.assertEquals("%s" % found.get("title"), user["job-title"]) + self.assertEquals("%s" % found.get("company"), user["company"]) + self.assertEquals("%s" % found.get("description"), user["description"]) + self.assertEquals("%s" % found.get("department"), user["department"]) + + def _check_posix_user(self, user): + """ check if a posix_user from SamDB has the same attributes as its template """ + found = self._find_user(user["name"]) + + self.assertEquals("%s" % found.get("loginShell"), user["loginShell"]) + self.assertEquals("%s" % found.get("gecos"), user["gecos"]) + self.assertEquals("%s" % found.get("uidNumber"), "%s" % user["uidNumber"]) + self.assertEquals("%s" % found.get("gidNumber"), "%s" % user["gidNumber"]) + self.assertEquals("%s" % found.get("uid"), user["uid"]) + self._check_user(user) + + def _create_user(self, user): + return self.runsubcmd("user", "add", user["name"], user["password"], + "--surname=%s" % user["surname"], + "--given-name=%s" % user["given-name"], + "--job-title=%s" % user["job-title"], + "--department=%s" % user["department"], + "--description=%s" % user["description"], + "--company=%s" % user["company"], + "-H", "ldap://%s" % os.environ["DC_SERVER"], + "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"])) + def _create_posix_user(self, user): + """ create a new user with RFC2307 attributes """ + return self.runsubcmd("user", "create", user["name"], user["password"], + "--surname=%s" % user["surname"], + "--given-name=%s" % user["given-name"], + "--job-title=%s" % user["job-title"], + "--department=%s" % user["department"], + "--description=%s" % user["description"], + "--company=%s" % user["company"], + "--gecos=%s" % user["gecos"], + "--login-shell=%s" % user["loginShell"], + "--uid=%s" % user["uid"], + "--uid-number=%s" % user["uidNumber"], + "--gid-number=%s" % user["gidNumber"], + "-H", "ldap://%s" % os.environ["DC_SERVER"], + "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"])) + + def _find_user(self, name): + search_filter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (ldb.binary_encode(name), "CN=Person,CN=Schema,CN=Configuration", self.samdb.domain_dn()) + userlist = self.samdb.search(base=self.samdb.domain_dn(), + scope=ldb.SCOPE_SUBTREE, + expression=search_filter, attrs=[]) + if userlist: + return userlist[0] + else: + return None diff --git a/python/samba/tests/samdb.py b/python/samba/tests/samdb.py new file mode 100644 index 00000000000..5c80391cbae --- /dev/null +++ b/python/samba/tests/samdb.py @@ -0,0 +1,96 @@ +# Unix SMB/CIFS implementation. Tests for SamDB +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2008 +# +# 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/>. +# + +"""Tests for samba.samdb.""" + +import logging +import os +import uuid + +from samba.auth import system_session +from samba.provision import (setup_samdb, guess_names, make_smbconf, + provision_paths_from_lp) +from samba.provision import DEFAULT_POLICY_GUID, DEFAULT_DC_POLICY_GUID +from samba.provision.backend import ProvisionBackend +from samba.tests import TestCaseInTempDir +from samba.dcerpc import security +from samba.schema import Schema +from samba import param + + +class SamDBTestCase(TestCaseInTempDir): + """Base-class for tests with a Sam Database. + + This is used by the Samba SamDB-tests, but e.g. also by the OpenChange + provisioning tests (which need a Sam). + """ + + def setUp(self): + super(SamDBTestCase, self).setUp() + invocationid = str(uuid.uuid4()) + domaindn = "DC=COM,DC=EXAMPLE" + self.domaindn = domaindn + configdn = "CN=Configuration," + domaindn + schemadn = "CN=Schema," + configdn + domainguid = str(uuid.uuid4()) + policyguid = DEFAULT_POLICY_GUID + domainsid = security.random_sid() + path = os.path.join(self.tempdir, "samdb.ldb") + session_info = system_session() + + hostname="foo" + domain="EXAMPLE" + dnsdomain="example.com" + serverrole="domain controller" + policyguid_dc = DEFAULT_DC_POLICY_GUID + + smbconf = os.path.join(self.tempdir, "smb.conf") + make_smbconf(smbconf, hostname, domain, dnsdomain, + self.tempdir, serverrole=serverrole) + + self.lp = param.LoadParm() + self.lp.load(smbconf) + + names = guess_names(lp=self.lp, hostname=hostname, + domain=domain, dnsdomain=dnsdomain, + serverrole=serverrole, + domaindn=self.domaindn, configdn=configdn, + schemadn=schemadn) + + paths = provision_paths_from_lp(self.lp, names.dnsdomain) + + logger = logging.getLogger("provision") + + provision_backend = ProvisionBackend("ldb", paths=paths, + lp=self.lp, credentials=None, + names=names, logger=logger) + + schema = Schema(domainsid, invocationid=invocationid, + schemadn=names.schemadn, serverdn=names.serverdn, + am_rodc=False) + + self.samdb = setup_samdb(path, session_info, + provision_backend, self.lp, names, logger, + domainsid, domainguid, policyguid, policyguid_dc, False, + "secret", "secret", "secret", invocationid, "secret", + None, "domain controller", schema=schema) + + def tearDown(self): + for f in ['schema.ldb', 'configuration.ldb', + 'users.ldb', 'samdb.ldb', 'smb.conf']: + os.remove(os.path.join(self.tempdir, f)) + super(SamDBTestCase, self).tearDown() diff --git a/python/samba/tests/security.py b/python/samba/tests/security.py new file mode 100644 index 00000000000..d2938aacb02 --- /dev/null +++ b/python/samba/tests/security.py @@ -0,0 +1,143 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007 +# +# 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/>. +# + +"""Tests for samba.dcerpc.security.""" + +import samba.tests +from samba.dcerpc import security + +class SecurityTokenTests(samba.tests.TestCase): + + def setUp(self): + super(SecurityTokenTests, self).setUp() + self.token = security.token() + + def test_is_system(self): + self.assertFalse(self.token.is_system()) + + def test_is_anonymous(self): + self.assertFalse(self.token.is_anonymous()) + + def test_has_builtin_administrators(self): + self.assertFalse(self.token.has_builtin_administrators()) + + def test_has_nt_authenticated_users(self): + self.assertFalse(self.token.has_nt_authenticated_users()) + + def test_has_priv(self): + self.assertFalse(self.token.has_privilege(security.SEC_PRIV_SHUTDOWN)) + + def test_set_priv(self): + self.assertFalse(self.token.has_privilege(security.SEC_PRIV_SHUTDOWN)) + self.assertFalse(self.token.set_privilege(security.SEC_PRIV_SHUTDOWN)) + self.assertTrue(self.token.has_privilege(security.SEC_PRIV_SHUTDOWN)) + + +class SecurityDescriptorTests(samba.tests.TestCase): + + def setUp(self): + super(SecurityDescriptorTests, self).setUp() + self.descriptor = security.descriptor() + + def test_from_sddl(self): + desc = security.descriptor.from_sddl("O:AOG:DAD:(A;;RPWPCCDCLCSWRCWDWOGA;;;S-1-0-0)", security.dom_sid("S-2-0-0")) + self.assertEquals(desc.group_sid, security.dom_sid('S-2-0-0-512')) + self.assertEquals(desc.owner_sid, security.dom_sid('S-1-5-32-548')) + self.assertEquals(desc.revision, 1) + self.assertEquals(desc.sacl, None) + self.assertEquals(desc.type, 0x8004) + + def test_from_sddl_invalidsddl(self): + self.assertRaises(TypeError,security.descriptor.from_sddl, "foo",security.dom_sid("S-2-0-0")) + + def test_from_sddl_invalidtype1(self): + self.assertRaises(TypeError, security.descriptor.from_sddl, security.dom_sid('S-2-0-0-512'),security.dom_sid("S-2-0-0")) + + def test_from_sddl_invalidtype2(self): + sddl = "O:AOG:DAD:(A;;RPWPCCDCLCSWRCWDWOGA;;;S-1-0-0)" + self.assertRaises(TypeError, security.descriptor.from_sddl, sddl, + "S-2-0-0") + + def test_as_sddl(self): + text = "O:AOG:DAD:(A;;RPWPCCDCLCSWRCWDWOGA;;;S-1-0-0)" + dom = security.dom_sid("S-2-0-0") + desc1 = security.descriptor.from_sddl(text, dom) + desc2 = security.descriptor.from_sddl(desc1.as_sddl(dom), dom) + self.assertEquals(desc1.group_sid, desc2.group_sid) + self.assertEquals(desc1.owner_sid, desc2.owner_sid) + self.assertEquals(desc1.sacl, desc2.sacl) + self.assertEquals(desc1.type, desc2.type) + + def test_as_sddl_invalid(self): + text = "O:AOG:DAD:(A;;RPWPCCDCLCSWRCWDWOGA;;;S-1-0-0)" + dom = security.dom_sid("S-2-0-0") + desc1 = security.descriptor.from_sddl(text, dom) + self.assertRaises(TypeError, desc1.as_sddl,text) + + + def test_as_sddl_no_domainsid(self): + dom = security.dom_sid("S-2-0-0") + text = "O:AOG:DAD:(A;;RPWPCCDCLCSWRCWDWOGA;;;S-1-0-0)" + desc1 = security.descriptor.from_sddl(text, dom) + desc2 = security.descriptor.from_sddl(desc1.as_sddl(), dom) + self.assertEquals(desc1.group_sid, desc2.group_sid) + self.assertEquals(desc1.owner_sid, desc2.owner_sid) + self.assertEquals(desc1.sacl, desc2.sacl) + self.assertEquals(desc1.type, desc2.type) + + def test_domsid_nodomsid_as_sddl(self): + dom = security.dom_sid("S-2-0-0") + text = "O:AOG:DAD:(A;;RPWPCCDCLCSWRCWDWOGA;;;S-1-0-0)" + desc1 = security.descriptor.from_sddl(text, dom) + self.assertNotEqual(desc1.as_sddl(), desc1.as_sddl(dom)) + + def test_split(self): + dom = security.dom_sid("S-2-0-7") + self.assertEquals((security.dom_sid("S-2-0"), 7), dom.split()) + + +class DomSidTests(samba.tests.TestCase): + + def test_parse_sid(self): + sid = security.dom_sid("S-1-5-21") + self.assertEquals("S-1-5-21", str(sid)) + + def test_sid_equal(self): + sid1 = security.dom_sid("S-1-5-21") + sid2 = security.dom_sid("S-1-5-21") + self.assertEquals(sid1, sid1) + self.assertEquals(sid1, sid2) + + def test_random(self): + sid = security.random_sid() + self.assertTrue(str(sid).startswith("S-1-5-21-")) + + def test_repr(self): + sid = security.random_sid() + self.assertTrue(repr(sid).startswith("dom_sid('S-1-5-21-")) + + +class PrivilegeTests(samba.tests.TestCase): + + def test_privilege_name(self): + self.assertEquals("SeShutdownPrivilege", + security.privilege_name(security.SEC_PRIV_SHUTDOWN)) + + def test_privilege_id(self): + self.assertEquals(security.SEC_PRIV_SHUTDOWN, + security.privilege_id("SeShutdownPrivilege")) + diff --git a/python/samba/tests/source.py b/python/samba/tests/source.py new file mode 100644 index 00000000000..2612ae68cf5 --- /dev/null +++ b/python/samba/tests/source.py @@ -0,0 +1,264 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2011 +# +# Loosely based on bzrlib's test_source.py +# +# 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/>. +# + +"""Source level Python tests.""" + +import errno +import os +import re +import warnings + +import samba +samba.ensure_external_module("pep8", "pep8") +import pep8 + +from samba.tests import ( + TestCase, + ) + + +def get_python_source_files(): + """Iterate over all Python source files.""" + library_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "samba")) + assert os.path.isdir(library_dir), library_dir + + for root, dirs, files in os.walk(library_dir): + for f in files: + if f.endswith(".py"): + yield os.path.abspath(os.path.join(root, f)) + + bindir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "..", "bin")) + assert os.path.isdir(bindir), bindir + for f in os.listdir(bindir): + p = os.path.abspath(os.path.join(bindir, f)) + if not os.path.islink(p): + continue + target = os.readlink(p) + if os.path.dirname(target).endswith("scripting/bin"): + yield p + wafsambadir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "..", "buildtools", "wafsamba")) + assert os.path.isdir(wafsambadir), wafsambadir + for root, dirs, files in os.walk(wafsambadir): + for f in files: + if f.endswith(".py"): + yield os.path.abspath(os.path.join(root, f)) + + +def get_source_file_contents(): + """Iterate over the contents of all python files.""" + for fname in get_python_source_files(): + try: + f = open(fname, 'rb') + except IOError, e: + if e.errno == errno.ENOENT: + warnings.warn("source file %s broken link?" % fname) + continue + else: + raise + try: + text = f.read() + finally: + f.close() + yield fname, text + + +class TestSource(TestCase): + + def test_copyright(self): + """Test that all Python files have a valid copyright statement.""" + incorrect = [] + + copyright_re = re.compile('#\\s*copyright.*(?=\n)', re.I) + + for fname, text in get_source_file_contents(): + if fname.endswith("ms_schema.py"): + # FIXME: Not sure who holds copyright on ms_schema.py + continue + if "wafsamba" in fname: + # FIXME: No copyright headers in wafsamba + continue + match = copyright_re.search(text) + if not match: + incorrect.append((fname, 'no copyright line found\n')) + + if incorrect: + help_text = ["Some files have missing or incorrect copyright" + " statements.", + "", + ] + for fname, comment in incorrect: + help_text.append(fname) + help_text.append((' ' * 4) + comment) + + self.fail('\n'.join(help_text)) + + def test_gpl(self): + """Test that all .py files have a GPL disclaimer.""" + incorrect = [] + + gpl_txt = """ +# 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/>. +""" + gpl_re = re.compile(re.escape(gpl_txt), re.MULTILINE) + + for fname, text in get_source_file_contents(): + if "wafsamba" in fname: + # FIXME: License to wafsamba hasn't been clarified yet + continue + if not gpl_re.search(text): + incorrect.append(fname) + + if incorrect: + help_text = ['Some files have missing or incomplete GPL statement', + gpl_txt] + for fname in incorrect: + help_text.append((' ' * 4) + fname) + + self.fail('\n'.join(help_text)) + + def _push_file(self, dict_, fname, line_no): + if fname not in dict_: + dict_[fname] = [line_no] + else: + dict_[fname].append(line_no) + + def _format_message(self, dict_, message): + files = ["%s: %s" % (f, ', '.join([str(i + 1) for i in lines])) + for f, lines in dict_.items()] + files.sort() + return message + '\n\n %s' % ('\n '.join(files)) + + def _iter_source_files_lines(self): + for fname, text in get_source_file_contents(): + lines = text.splitlines(True) + last_line_no = len(lines) - 1 + for line_no, line in enumerate(lines): + yield fname, line_no, line + + def test_no_tabs(self): + """Check that there are no tabs in Python files.""" + tabs = {} + for fname, line_no, line in self._iter_source_files_lines(): + if '\t' in line: + self._push_file(tabs, fname, line_no) + if tabs: + self.fail(self._format_message(tabs, + 'Tab characters were found in the following source files.' + '\nThey should either be replaced by "\\t" or by spaces:')) + + def test_unix_newlines(self): + """Check for unix new lines.""" + illegal_newlines = {} + for fname, line_no, line in self._iter_source_files_lines(): + if not line.endswith('\n') or line.endswith('\r\n'): + self._push_file(illegal_newlines, fname, line_no) + if illegal_newlines: + self.fail(self._format_message(illegal_newlines, + 'Non-unix newlines were found in the following source files:')) + + def test_trailing_whitespace(self): + """Check that there is not trailing whitespace in Python files.""" + trailing_whitespace = {} + for fname, line_no, line in self._iter_source_files_lines(): + if line.rstrip("\n").endswith(" "): + self._push_file(trailing_whitespace, fname, line_no) + if trailing_whitespace: + self.fail(self._format_message(trailing_whitespace, + 'Trailing whitespace was found in the following source files.')) + + def test_shebang_lines(self): + """Check that files with shebang lines and only those are executable.""" + files_with_shebang = {} + files_without_shebang= {} + for fname, line_no, line in self._iter_source_files_lines(): + if line_no >= 1: + continue + executable = (os.stat(fname).st_mode & 0111) + has_shebang = line.startswith("#!") + if has_shebang and not executable: + self._push_file(files_with_shebang, fname, line_no) + if not has_shebang and executable: + self._push_file(files_without_shebang, fname, line_no) + if files_with_shebang: + self.fail(self._format_message(files_with_shebang, + 'Files with shebang line that are not executable:')) + if files_without_shebang: + self.fail(self._format_message(files_without_shebang, + 'Files without shebang line that are executable:')) + + pep8_ignore = [ + 'E401', # multiple imports on one line + 'E501', # line too long + 'E251', # no spaces around keyword / parameter equals + 'E201', # whitespace after '[' + 'E202', # whitespace before ')' + 'E302', # expected 2 blank lines, found 1 + 'E231', # missing whitespace after ',' + 'E225', # missing whitespace around operator + 'E111', # indentation is not a multiple of four + 'E261', # at least two spaces before inline comment + 'E702', # multiple statements on one line (semicolon) + 'E221', # multiple spaces before operator + 'E303', # too many blank lines (2) + 'E203', # whitespace before ':' + 'E222', # multiple spaces after operator + 'E301', # expected 1 blank line, found 0 + 'E211', # whitespace before '(' + 'E701', # multiple statements on one line (colon) + ] + + def test_pep8(self): + pep8.process_options() + pep8.options.repeat = True + pep8_errors = [] + pep8_warnings = [] + for fname, text in get_source_file_contents(): + def report_error(line_number, offset, text, check): + code = text[:4] + if code in self.pep8_ignore: + code = 'W' + code[1:] + text = code + text[4:] + print "%s:%s: %s" % (fname, line_number, text) + summary = (fname, line_number, offset, text, check) + if code[0] == 'W': + pep8_warnings.append(summary) + else: + pep8_errors.append(summary) + lines = text.splitlines(True) + checker = pep8.Checker(fname, lines) + checker.report_error = report_error + checker.check_all() + if len(pep8_errors) > 0: + d = {} + for (fname, line_no, offset, text, check) in pep8_errors: + d.setdefault(fname, []).append(line_no - 1) + self.fail(self._format_message(d, + 'There were %d PEP8 errors:' % len(pep8_errors))) + diff --git a/python/samba/tests/strings.py b/python/samba/tests/strings.py new file mode 100644 index 00000000000..23382d756ec --- /dev/null +++ b/python/samba/tests/strings.py @@ -0,0 +1,103 @@ +# subunit test cases for Samba string functions. + +# Copyright (C) 2003 by Martin Pool <mbp@samba.org> +# Copyright (C) 2011 Andrew Bartlett +# +# 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/>. +# + +# XXX: All this code assumes that the Unix character set is UTF-8, +# which is the most common setting. I guess it would be better to +# force it to that value while running the tests. I'm not sure of the +# best way to do that yet. +# +# -- mbp + +from unicodenames import * + +import samba.tests +from samba import strcasecmp_m, strstr_m + +def signum(a): + if a < 0: + return -1 + elif a > 0: + return +1 + else: + return 0 + + +class strcasecmp_m_Tests(samba.tests.TestCase): + """String comparisons in simple ASCII and unicode""" + def test_strcasecmp_m(self): + # A, B, strcasecmp(A, B) + cases = [('hello', 'hello', 0), + ('hello', 'goodbye', +1), + ('goodbye', 'hello', -1), + ('hell', 'hello', -1), + ('', '', 0), + ('a', '', +1), + ('', 'a', -1), + ('a', 'A', 0), + ('aa', 'aA', 0), + ('Aa', 'aa', 0), + ('longstring ' * 100, 'longstring ' * 100, 0), + ('longstring ' * 100, 'longstring ' * 100 + 'a', -1), + ('longstring ' * 100 + 'a', 'longstring ' * 100, +1), + (KATAKANA_LETTER_A, KATAKANA_LETTER_A, 0), + (KATAKANA_LETTER_A, 'a', 1), + ] + for a, b, expect in cases: + self.assertEquals(signum(strcasecmp_m(a.encode('utf-8'), + b.encode('utf-8'))), + expect) + +class strstr_m_Tests(samba.tests.TestCase): + """strstr_m tests in simple ASCII and unicode strings""" + + def test_strstr_m(self): + # A, B, strstr_m(A, B) + cases = [('hello', 'hello', 'hello'), + ('hello', 'goodbye', None), + ('goodbye', 'hello', None), + ('hell', 'hello', None), + ('hello', 'hell', 'hello'), + ('', '', ''), + ('a', '', 'a'), + ('', 'a', None), + ('a', 'A', None), + ('aa', 'aA', None), + ('Aa', 'aa', None), + ('%v foo', '%v', '%v foo'), + ('foo %v foo', '%v', '%v foo'), + ('foo %v', '%v', '%v'), + ('longstring ' * 100, 'longstring ' * 99, 'longstring ' * 100), + ('longstring ' * 99, 'longstring ' * 100, None), + ('longstring a' * 99, 'longstring ' * 100 + 'a', None), + ('longstring ' * 100 + 'a', 'longstring ' * 100, 'longstring ' * 100 + 'a'), + (KATAKANA_LETTER_A, KATAKANA_LETTER_A + 'bcd', None), + (KATAKANA_LETTER_A + 'bcde', KATAKANA_LETTER_A + 'bcd', KATAKANA_LETTER_A + 'bcde'), + ('d'+KATAKANA_LETTER_A + 'bcd', KATAKANA_LETTER_A + 'bcd', KATAKANA_LETTER_A + 'bcd'), + ('d'+KATAKANA_LETTER_A + 'bd', KATAKANA_LETTER_A + 'bcd', None), + + ('e'+KATAKANA_LETTER_A + 'bcdf', KATAKANA_LETTER_A + 'bcd', KATAKANA_LETTER_A + 'bcdf'), + (KATAKANA_LETTER_A, KATAKANA_LETTER_A + 'bcd', None), + (KATAKANA_LETTER_A*3, 'a', None), + ] + for a, b, expect in cases: + if expect is not None: + expect = expect.encode('utf-8') + self.assertEquals(strstr_m(a.encode('utf-8'), + b.encode('utf-8')), + expect) diff --git a/python/samba/tests/unicodenames.py b/python/samba/tests/unicodenames.py new file mode 100644 index 00000000000..ed65de6651f --- /dev/null +++ b/python/samba/tests/unicodenames.py @@ -0,0 +1,29 @@ +# Copyright (C) 2003 by Martin Pool <mbp@samba.org> + +# 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/>. +# + +""" +Defines symbolic names for a few UNICODE characters, to make test +source code more readable on machines that don't have all the +necessary fonts. + +You can do "import *" on this file safely. +""" + +LATIN_CAPITAL_LETTER_N_WITH_TILDE = u'\u004e' +LATIN_CAPITAL_LETTER_O_WITH_DIARESIS = u'\u00d6' +LATIN_SMALL_LETTER_O_WITH_DIARESIS = u'\u00f6' + +KATAKANA_LETTER_A = u'\u30a2' diff --git a/python/samba/tests/upgrade.py b/python/samba/tests/upgrade.py new file mode 100644 index 00000000000..b46a4173191 --- /dev/null +++ b/python/samba/tests/upgrade.py @@ -0,0 +1,40 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007 +# +# 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/>. +# + +"""Tests for samba.upgrade.""" + +from samba.upgrade import import_wins +from samba.tests import LdbTestCase + + +class WinsUpgradeTests(LdbTestCase): + + def test_upgrade(self): + winsdb = { + "FOO#20": (200, ["127.0.0.1", "127.0.0.2"], 0x60) + } + import_wins(self.ldb, winsdb) + + self.assertEquals( + ['name=FOO,type=0x20'], + [str(m.dn) for m in + self.ldb.search(expression="(objectClass=winsRecord)")]) + + def test_version(self): + import_wins(self.ldb, {}) + self.assertEquals("VERSION", + str(self.ldb.search(expression="(objectClass=winsMaxVersion)")[0]["cn"])) diff --git a/python/samba/tests/upgradeprovision.py b/python/samba/tests/upgradeprovision.py new file mode 100644 index 00000000000..93a6731c830 --- /dev/null +++ b/python/samba/tests/upgradeprovision.py @@ -0,0 +1,135 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2008 +# +# 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/>. +# + +"""Tests for samba.upgradeprovision.""" + +import os +from samba.upgradehelpers import (usn_in_range, dn_sort, + get_diff_sddls, update_secrets, + construct_existor_expr) + +from samba.tests.provision import create_dummy_secretsdb +from samba.tests import TestCaseInTempDir +from samba import Ldb +from ldb import SCOPE_BASE +import samba.tests + +def dummymessage(a=None, b=None): + pass + + +class UpgradeProvisionTestCase(TestCaseInTempDir): + """Some simple tests for individual functions in the provisioning code. + """ + def test_usn_in_range(self): + range = [5, 25, 35, 55] + + vals = [3, 26, 56] + + for v in vals: + self.assertFalse(usn_in_range(v, range)) + + vals = [5, 20, 25, 35, 36] + + for v in vals: + self.assertTrue(usn_in_range(v, range)) + + def test_dn_sort(self): + # higher level comes after lower even if lexicographicaly closer + # ie dc=tata,dc=toto (2 levels), comes after dc=toto + # even if dc=toto is lexicographicaly after dc=tata, dc=toto + self.assertEquals(dn_sort("dc=tata,dc=toto", "dc=toto"), 1) + self.assertEquals(dn_sort("dc=zata", "dc=tata"), 1) + self.assertEquals(dn_sort("dc=toto,dc=tata", + "cn=foo,dc=toto,dc=tata"), -1) + self.assertEquals(dn_sort("cn=bar, dc=toto,dc=tata", + "cn=foo, dc=toto,dc=tata"), -1) + + def test_get_diff_sddl(self): + sddl = "O:SAG:DUD:AI(A;CIID;RPWPCRCCLCLORCWOWDSW;;;SA)\ +(A;CIID;RP LCLORC;;;AU)(A;CIID;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)S:AI(AU;CIIDSA;WP;;;WD)" + sddl1 = "O:SAG:DUD:AI(A;CIID;RPWPCRCCLCLORCWOWDSW;;;SA)\ +(A;CIID;RP LCLORC;;;AU)(A;CIID;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)S:AI(AU;CIIDSA;WP;;;WD)" + sddl2 = "O:BAG:DUD:AI(A;CIID;RPWPCRCCLCLORCWOWDSW;;;SA)\ +(A;CIID;RP LCLORC;;;AU)(A;CIID;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)S:AI(AU;CIIDSA;WP;;;WD)" + sddl3 = "O:SAG:BAD:AI(A;CIID;RPWPCRCCLCLORCWOWDSW;;;SA)\ +(A;CIID;RP LCLORC;;;AU)(A;CIID;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)S:AI(AU;CIIDSA;WP;;;WD)" + sddl4 = "O:SAG:DUD:AI(A;CIID;RPWPCRCCLCLORCWOWDSW;;;BA)\ +(A;CIID;RP LCLORC;;;AU)(A;CIID;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)S:AI(AU;CIIDSA;WP;;;WD)" + sddl5 = "O:SAG:DUD:AI(A;CIID;RPWPCRCCLCLORCWOWDSW;;;SA)\ +(A;CIID;RP LCLORC;;;AU)(A;CIID;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)" + + self.assertEquals(get_diff_sddls(sddl, sddl1), "") + txt = get_diff_sddls(sddl, sddl2) + self.assertEquals(txt, "\tOwner mismatch: SA (in ref) BA(in current)\n") + txt = get_diff_sddls(sddl, sddl3) + self.assertEquals(txt, "\tGroup mismatch: DU (in ref) BA(in current)\n") + txt = get_diff_sddls(sddl, sddl4) + txtmsg = "\tPart dacl is different between reference and current here\ + is the detail:\n\t\t(A;CIID;RPWPCRCCLCLORCWOWDSW;;;BA) ACE is not present in\ + the reference\n\t\t(A;CIID;RPWPCRCCLCLORCWOWDSW;;;SA) ACE is not present in\ + the current\n" + self.assertEquals(txt, txtmsg) + txt = get_diff_sddls(sddl, sddl5) + self.assertEquals(txt, "\tCurrent ACL hasn't a sacl part\n") + + def test_construct_existor_expr(self): + res = construct_existor_expr([]) + self.assertEquals(res, "") + + res = construct_existor_expr(["foo"]) + self.assertEquals(res, "(|(foo=*))") + + res = construct_existor_expr(["foo", "bar"]) + self.assertEquals(res, "(|(foo=*)(bar=*))") + + +class UpdateSecretsTests(samba.tests.TestCaseInTempDir): + + def setUp(self): + super(UpdateSecretsTests, self).setUp() + self.referencedb = create_dummy_secretsdb( + os.path.join(self.tempdir, "ref.ldb")) + + def _getEmptyDb(self): + return Ldb(os.path.join(self.tempdir, "secrets.ldb")) + + def _getCurrentFormatDb(self): + return create_dummy_secretsdb( + os.path.join(self.tempdir, "secrets.ldb")) + + def test_trivial(self): + # Test that updating an already up-to-date secretsdb works fine + self.secretsdb = self._getCurrentFormatDb() + self.assertEquals(None, + update_secrets(self.referencedb, self.secretsdb, dummymessage)) + + def test_update_modules(self): + empty_db = self._getEmptyDb() + update_secrets(self.referencedb, empty_db, dummymessage) + newmodules = empty_db.search(base="@MODULES", scope=SCOPE_BASE) + refmodules = self.referencedb.search(base="@MODULES", scope=SCOPE_BASE) + self.assertEquals(newmodules.msgs, refmodules.msgs) + + def tearDown(self): + for name in ["ref.ldb", "secrets.ldb", "secrets.tdb", "secrets.tdb.bak", "secrets.ntdb"]: + path = os.path.join(self.tempdir, name) + if os.path.exists(path): + os.unlink(path) + super(UpdateSecretsTests, self).tearDown() + + diff --git a/python/samba/tests/upgradeprovisionneeddc.py b/python/samba/tests/upgradeprovisionneeddc.py new file mode 100644 index 00000000000..a7cb298ed7f --- /dev/null +++ b/python/samba/tests/upgradeprovisionneeddc.py @@ -0,0 +1,179 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2008 +# +# 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/>. +# + +"""Tests for samba.upgradeprovision that need a DC.""" + +import os +import re +import shutil + +from samba import param +from samba.credentials import Credentials +from samba.auth import system_session +from samba.provision import getpolicypath,find_provision_key_parameters +from samba.upgradehelpers import (get_paths, get_ldbs, + identic_rename, + updateOEMInfo, getOEMInfo, update_gpo, + delta_update_basesamdb, + update_dns_account_password, + search_constructed_attrs_stored, + increment_calculated_keyversion_number) +from samba.tests import env_loadparm, TestCaseInTempDir +from samba.tests.provision import create_dummy_secretsdb +import ldb + + +def dummymessage(a=None, b=None): + pass + +smb_conf_path = "%s/%s/%s" % (os.environ["SELFTEST_PREFIX"], "dc", "etc/smb.conf") + +class UpgradeProvisionBasicLdbHelpersTestCase(TestCaseInTempDir): + """Some simple tests for individual functions in the provisioning code. + """ + + def test_get_ldbs(self): + paths = get_paths(param, None, smb_conf_path) + creds = Credentials() + lp = env_loadparm() + creds.guess(lp) + get_ldbs(paths, creds, system_session(), lp) + + def test_find_key_param(self): + paths = get_paths(param, None, smb_conf_path) + creds = Credentials() + lp = env_loadparm() + creds.guess(lp) + rootdn = "dc=samba,dc=example,dc=com" + ldbs = get_ldbs(paths, creds, system_session(), lp) + names = find_provision_key_parameters(ldbs.sam, ldbs.secrets, ldbs.idmap, + paths, smb_conf_path, lp) + self.assertEquals(names.realm, "SAMBA.EXAMPLE.COM") + self.assertEquals(str(names.rootdn).lower(), rootdn.lower()) + self.assertNotEquals(names.policyid_dc, None) + self.assertNotEquals(names.ntdsguid, "") + + +class UpgradeProvisionWithLdbTestCase(TestCaseInTempDir): + + def _getEmptyDbName(self): + return os.path.join(self.tempdir, "sam.ldb") + + def setUp(self): + super(UpgradeProvisionWithLdbTestCase, self).setUp() + paths = get_paths(param, None, smb_conf_path) + self.creds = Credentials() + self.lp = env_loadparm() + self.creds.guess(self.lp) + self.paths = paths + self.ldbs = get_ldbs(paths, self.creds, system_session(), self.lp) + self.names = find_provision_key_parameters(self.ldbs.sam, + self.ldbs.secrets, self.ldbs.idmap, paths, smb_conf_path, + self.lp) + self.referencedb = create_dummy_secretsdb( + os.path.join(self.tempdir, "ref.ldb")) + + def test_search_constructed_attrs_stored(self): + hashAtt = search_constructed_attrs_stored(self.ldbs.sam, + self.names.rootdn, + ["msds-KeyVersionNumber"]) + self.assertFalse(hashAtt.has_key("msds-KeyVersionNumber")) + + def test_increment_calculated_keyversion_number(self): + dn = "CN=Administrator,CN=Users,%s" % self.names.rootdn + # We conctruct a simple hash for the user administrator + hash = {} + # And we want the version to be 140 + hash[dn.lower()] = 140 + + increment_calculated_keyversion_number(self.ldbs.sam, + self.names.rootdn, + hash) + self.assertEqual(self.ldbs.sam.get_attribute_replmetadata_version(dn, + "unicodePwd"), + 140) + # This function should not decrement the version + hash[dn.lower()] = 130 + + increment_calculated_keyversion_number(self.ldbs.sam, + self.names.rootdn, + hash) + self.assertEqual(self.ldbs.sam.get_attribute_replmetadata_version(dn, + "unicodePwd"), + 140) + + def test_identic_rename(self): + rootdn = "DC=samba,DC=example,DC=com" + + guestDN = ldb.Dn(self.ldbs.sam, "CN=Guest,CN=Users,%s" % rootdn) + identic_rename(self.ldbs.sam, guestDN) + res = self.ldbs.sam.search(expression="(name=Guest)", base=rootdn, + scope=ldb.SCOPE_SUBTREE, attrs=["dn"]) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0]["dn"]), "CN=Guest,CN=Users,%s" % rootdn) + + def test_delta_update_basesamdb(self): + dummysampath = self._getEmptyDbName() + delta_update_basesamdb(self.paths.samdb, dummysampath, + self.creds, system_session(), self.lp, + dummymessage) + + def test_update_gpo_simple(self): + dir = getpolicypath(self.paths.sysvol, self.names.dnsdomain, + self.names.policyid) + shutil.rmtree(dir) + self.assertFalse(os.path.isdir(dir)) + update_gpo(self.paths, self.ldbs.sam, self.names, self.lp, dummymessage) + self.assertTrue(os.path.isdir(dir)) + + def test_update_gpo_acl(self): + path = os.path.join(self.tempdir, "testupdategpo") + save = self.paths.sysvol + self.paths.sysvol = path + os.mkdir(path) + os.mkdir(os.path.join(path, self.names.dnsdomain)) + os.mkdir(os.path.join(os.path.join(path, self.names.dnsdomain), + "Policies")) + update_gpo(self.paths, self.ldbs.sam, self.names, self.lp, dummymessage) + shutil.rmtree(path) + self.paths.sysvol = save + + def test_getOEMInfo(self): + realm = self.lp.get("realm") + basedn = "DC=%s" % realm.replace(".", ", DC=") + oem = getOEMInfo(self.ldbs.sam, basedn) + self.assertNotEquals(oem, "") + + def test_update_dns_account(self): + update_dns_account_password(self.ldbs.sam, self.ldbs.secrets, + self.names) + + def test_updateOEMInfo(self): + realm = self.lp.get("realm") + basedn = "DC=%s" % realm.replace(".", ", DC=") + oem = getOEMInfo(self.ldbs.sam, basedn) + updateOEMInfo(self.ldbs.sam, basedn) + oem2 = getOEMInfo(self.ldbs.sam, basedn) + self.assertNotEquals(str(oem), str(oem2)) + self.assertTrue(re.match(".*upgrade to.*", str(oem2))) + + def tearDown(self): + for name in ["ref.ldb", "secrets.ldb", "secrets.tdb", "secrets.tdb.bak", "secrets.ntdb", "sam.ldb"]: + path = os.path.join(self.tempdir, name) + if os.path.exists(path): + os.unlink(path) + super(UpgradeProvisionWithLdbTestCase, self).tearDown() diff --git a/python/samba/tests/xattr.py b/python/samba/tests/xattr.py new file mode 100644 index 00000000000..89add284566 --- /dev/null +++ b/python/samba/tests/xattr.py @@ -0,0 +1,126 @@ +# Unix SMB/CIFS implementation. Tests for xattr manipulation +# Copyright (C) Matthieu Patou <mat@matws.net> 2009 +# +# 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/>. +# + +"""Tests for samba.xattr_native and samba.xattr_tdb.""" + +import samba.xattr_native, samba.xattr_tdb +from samba.xattr import copytree_with_xattrs +from samba.dcerpc import xattr +from samba.ndr import ndr_pack +from samba.tests import ( + TestCase, + TestCaseInTempDir, + TestSkipped, + ) +import random +import shutil +import os + +class XattrTests(TestCase): + + def _tmpfilename(self): + random.seed() + path = os.environ['SELFTEST_PREFIX'] + return os.path.join(path, "pytests"+str(int(100000*random.random()))) + + def _eadbpath(self): + return os.path.join(os.environ['SELFTEST_PREFIX'], "eadb.tdb") + + def test_set_xattr_native(self): + if not samba.xattr_native.is_xattr_supported(): + raise TestSkipped() + ntacl = xattr.NTACL() + ntacl.version = 1 + tempf = self._tmpfilename() + open(tempf, 'w').write("empty") + try: + samba.xattr_native.wrap_setxattr(tempf, "user.unittests", + ndr_pack(ntacl)) + except IOError: + raise TestSkipped("the filesystem where the tests are runned do not support XATTR") + os.unlink(tempf) + + def test_set_and_get_native(self): + if not samba.xattr_native.is_xattr_supported(): + raise TestSkipped() + tempf = self._tmpfilename() + reftxt = "this is a test" + open(tempf, 'w').write("empty") + try: + samba.xattr_native.wrap_setxattr(tempf, "user.unittests", reftxt) + text = samba.xattr_native.wrap_getxattr(tempf, "user.unittests") + self.assertEquals(text, reftxt) + except IOError: + raise TestSkipped("the filesystem where the tests are runned do not support XATTR") + os.unlink(tempf) + + def test_set_xattr_tdb(self): + tempf = self._tmpfilename() + eadb_path = self._eadbpath() + ntacl = xattr.NTACL() + ntacl.version = 1 + open(tempf, 'w').write("empty") + try: + samba.xattr_tdb.wrap_setxattr(eadb_path, + tempf, "user.unittests", ndr_pack(ntacl)) + finally: + os.unlink(tempf) + os.unlink(eadb_path) + + def test_set_tdb_not_open(self): + tempf = self._tmpfilename() + ntacl = xattr.NTACL() + ntacl.version = 1 + open(tempf, 'w').write("empty") + try: + self.assertRaises(IOError, samba.xattr_tdb.wrap_setxattr, + os.path.join("nonexistent", "eadb.tdb"), tempf, + "user.unittests", ndr_pack(ntacl)) + finally: + os.unlink(tempf) + + def test_set_and_get_tdb(self): + tempf = self._tmpfilename() + eadb_path = self._eadbpath() + reftxt = "this is a test" + open(tempf, 'w').write("empty") + try: + samba.xattr_tdb.wrap_setxattr(eadb_path, tempf, "user.unittests", + reftxt) + text = samba.xattr_tdb.wrap_getxattr(eadb_path, tempf, + "user.unittests") + self.assertEquals(text, reftxt) + finally: + os.unlink(tempf) + os.unlink(eadb_path) + + +class TestCopyTreeWithXattrs(TestCaseInTempDir): + + def test_simple(self): + os.chdir(self.tempdir) + os.mkdir("a") + os.mkdir("a/b") + os.mkdir("a/b/c") + f = open('a/b/c/d', 'w') + try: + f.write("foo") + finally: + f.close() + copytree_with_xattrs("a", "b") + shutil.rmtree("a") + shutil.rmtree("b") diff --git a/python/samba/upgrade.py b/python/samba/upgrade.py new file mode 100644 index 00000000000..d680a7ca235 --- /dev/null +++ b/python/samba/upgrade.py @@ -0,0 +1,938 @@ +# backend code for upgrading from Samba3 +# Copyright Jelmer Vernooij 2005-2007 +# Copyright Andrew Bartlett 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/>. +# + +"""Support code for upgrading from Samba 3 to Samba 4.""" + +__docformat__ = "restructuredText" + +import ldb +import time +import pwd + +from samba import Ldb, registry +from samba.param import LoadParm +from samba.provision import provision, FILL_FULL, ProvisioningError, setsysvolacl +from samba.samba3 import passdb +from samba.samba3 import param as s3param +from samba.dcerpc import lsa, samr, security +from samba.dcerpc.security import dom_sid +from samba.credentials import Credentials +from samba import dsdb +from samba.ndr import ndr_pack +from samba import unix2nttime +from samba import generate_random_password + + +def import_sam_policy(samdb, policy, logger): + """Import a Samba 3 policy. + + :param samdb: Samba4 SAM database + :param policy: Samba3 account policy + :param logger: Logger object + """ + + # Following entries are used - + # min password length, password history, minimum password age, + # maximum password age, lockout duration + # + # Following entries are not used - + # reset count minutes, user must logon to change password, + # bad lockout minutes, disconnect time + + m = ldb.Message() + m.dn = samdb.get_default_basedn() + + if 'min password length' in policy: + m['a01'] = ldb.MessageElement(str(policy['min password length']), + ldb.FLAG_MOD_REPLACE, 'minPwdLength') + + if 'password history' in policy: + m['a02'] = ldb.MessageElement(str(policy['password history']), + ldb.FLAG_MOD_REPLACE, 'pwdHistoryLength') + + if 'minimum password age' in policy: + min_pw_age_unix = policy['minimum password age'] + min_pw_age_nt = int(-min_pw_age_unix * (1e7)) + m['a03'] = ldb.MessageElement(str(min_pw_age_nt), ldb.FLAG_MOD_REPLACE, + 'minPwdAge') + + if 'maximum password age' in policy: + max_pw_age_unix = policy['maximum password age'] + if max_pw_age_unix == -1 or max_pw_age_unix == 0: + max_pw_age_nt = -0x8000000000000000 + else: + max_pw_age_nt = int(-max_pw_age_unix * (1e7)) + + m['a04'] = ldb.MessageElement(str(max_pw_age_nt), ldb.FLAG_MOD_REPLACE, + 'maxPwdAge') + + if 'lockout duration' in policy: + lockout_duration_mins = policy['lockout duration'] + lockout_duration_nt = unix2nttime(lockout_duration_mins * 60) + + m['a05'] = ldb.MessageElement(str(lockout_duration_nt), + ldb.FLAG_MOD_REPLACE, 'lockoutDuration') + + try: + samdb.modify(m) + except ldb.LdbError, e: + logger.warn("Could not set account policy, (%s)", str(e)) + + +def add_posix_attrs(logger, samdb, sid, name, nisdomain, xid_type, home=None, + shell=None, pgid=None): + """Add posix attributes for the user/group + + :param samdb: Samba4 sam.ldb database + :param sid: user/group sid + :param sid: user/group name + :param nisdomain: name of the (fake) NIS domain + :param xid_type: type of id (ID_TYPE_UID/ID_TYPE_GID) + :param home: user homedir (Unix homepath) + :param shell: user shell + :param pgid: users primary group id + """ + + try: + m = ldb.Message() + m.dn = ldb.Dn(samdb, "<SID=%s>" % str(sid)) + if xid_type == "ID_TYPE_UID": + m['unixHomeDirectory'] = ldb.MessageElement( + str(home), ldb.FLAG_MOD_REPLACE, 'unixHomeDirectory') + m['loginShell'] = ldb.MessageElement( + str(shell), ldb.FLAG_MOD_REPLACE, 'loginShell') + m['gidNumber'] = ldb.MessageElement( + str(pgid), ldb.FLAG_MOD_REPLACE, 'gidNumber') + + m['msSFU30NisDomain'] = ldb.MessageElement( + str(nisdomain), ldb.FLAG_MOD_REPLACE, 'msSFU30NisDomain') + + samdb.modify(m) + except ldb.LdbError, e: + logger.warn( + 'Could not add posix attrs for AD entry for sid=%s, (%s)', + str(sid), str(e)) + +def add_ad_posix_idmap_entry(samdb, sid, xid, xid_type, logger): + """Create idmap entry + + :param samdb: Samba4 sam.ldb database + :param sid: user/group sid + :param xid: user/group id + :param xid_type: type of id (ID_TYPE_UID/ID_TYPE_GID) + :param logger: Logger object + """ + + try: + m = ldb.Message() + m.dn = ldb.Dn(samdb, "<SID=%s>" % str(sid)) + if xid_type == "ID_TYPE_UID": + m['uidNumber'] = ldb.MessageElement( + str(xid), ldb.FLAG_MOD_REPLACE, 'uidNumber') + m['objectClass'] = ldb.MessageElement( + "posixAccount", ldb.FLAG_MOD_ADD, 'objectClass') + elif xid_type == "ID_TYPE_GID": + m['gidNumber'] = ldb.MessageElement( + str(xid), ldb.FLAG_MOD_REPLACE, 'gidNumber') + m['objectClass'] = ldb.MessageElement( + "posixGroup", ldb.FLAG_MOD_ADD, 'objectClass') + + samdb.modify(m) + except ldb.LdbError, e: + logger.warn( + 'Could not modify AD idmap entry for sid=%s, id=%s, type=%s (%s)', + str(sid), str(xid), xid_type, str(e)) + + +def add_idmap_entry(idmapdb, sid, xid, xid_type, logger): + """Create idmap entry + + :param idmapdb: Samba4 IDMAP database + :param sid: user/group sid + :param xid: user/group id + :param xid_type: type of id (ID_TYPE_UID/ID_TYPE_GID) + :param logger: Logger object + """ + + # First try to see if we already have this entry + found = False + msg = idmapdb.search(expression='objectSid=%s' % str(sid)) + if msg.count == 1: + found = True + + if found: + try: + m = ldb.Message() + m.dn = msg[0]['dn'] + m['xidNumber'] = ldb.MessageElement( + str(xid), ldb.FLAG_MOD_REPLACE, 'xidNumber') + m['type'] = ldb.MessageElement( + xid_type, ldb.FLAG_MOD_REPLACE, 'type') + idmapdb.modify(m) + except ldb.LdbError, e: + logger.warn( + 'Could not modify idmap entry for sid=%s, id=%s, type=%s (%s)', + str(sid), str(xid), xid_type, str(e)) + else: + try: + idmapdb.add({"dn": "CN=%s" % str(sid), + "cn": str(sid), + "objectClass": "sidMap", + "objectSid": ndr_pack(sid), + "type": xid_type, + "xidNumber": str(xid)}) + except ldb.LdbError, e: + logger.warn( + 'Could not add idmap entry for sid=%s, id=%s, type=%s (%s)', + str(sid), str(xid), xid_type, str(e)) + + +def import_idmap(idmapdb, samba3, logger): + """Import idmap data. + + :param idmapdb: Samba4 IDMAP database + :param samba3_idmap: Samba3 IDMAP database to import from + :param logger: Logger object + """ + + try: + samba3_idmap = samba3.get_idmap_db() + except IOError, e: + logger.warn('Cannot open idmap database, Ignoring: %s', str(e)) + return + + currentxid = max(samba3_idmap.get_user_hwm(), samba3_idmap.get_group_hwm()) + lowerbound = currentxid + # FIXME: upperbound + + m = ldb.Message() + m.dn = ldb.Dn(idmapdb, 'CN=CONFIG') + m['lowerbound'] = ldb.MessageElement( + str(lowerbound), ldb.FLAG_MOD_REPLACE, 'lowerBound') + m['xidNumber'] = ldb.MessageElement( + str(currentxid), ldb.FLAG_MOD_REPLACE, 'xidNumber') + idmapdb.modify(m) + + for id_type, xid in samba3_idmap.ids(): + if id_type == 'UID': + xid_type = 'ID_TYPE_UID' + elif id_type == 'GID': + xid_type = 'ID_TYPE_GID' + else: + logger.warn('Wrong type of entry in idmap (%s), Ignoring', id_type) + continue + + sid = samba3_idmap.get_sid(xid, id_type) + add_idmap_entry(idmapdb, dom_sid(sid), xid, xid_type, logger) + + +def add_group_from_mapping_entry(samdb, groupmap, logger): + """Add or modify group from group mapping entry + + param samdb: Samba4 SAM database + param groupmap: Groupmap entry + param logger: Logger object + """ + + # First try to see if we already have this entry + try: + msg = samdb.search( + base='<SID=%s>' % str(groupmap.sid), scope=ldb.SCOPE_BASE) + found = True + except ldb.LdbError, (ecode, emsg): + if ecode == ldb.ERR_NO_SUCH_OBJECT: + found = False + else: + raise ldb.LdbError(ecode, emsg) + + if found: + logger.warn('Group already exists sid=%s, groupname=%s existing_groupname=%s, Ignoring.', + str(groupmap.sid), groupmap.nt_name, msg[0]['sAMAccountName'][0]) + else: + if groupmap.sid_name_use == lsa.SID_NAME_WKN_GRP: + # In a lot of Samba3 databases, aliases are marked as well known groups + (group_dom_sid, rid) = groupmap.sid.split() + if (group_dom_sid != security.dom_sid(security.SID_BUILTIN)): + return + + m = ldb.Message() + m.dn = ldb.Dn(samdb, "CN=%s,CN=Users,%s" % (groupmap.nt_name, samdb.get_default_basedn())) + m['cn'] = ldb.MessageElement(groupmap.nt_name, ldb.FLAG_MOD_ADD, 'cn') + m['objectClass'] = ldb.MessageElement('group', ldb.FLAG_MOD_ADD, 'objectClass') + m['objectSid'] = ldb.MessageElement(ndr_pack(groupmap.sid), ldb.FLAG_MOD_ADD, + 'objectSid') + m['sAMAccountName'] = ldb.MessageElement(groupmap.nt_name, ldb.FLAG_MOD_ADD, + 'sAMAccountName') + + if groupmap.comment: + m['description'] = ldb.MessageElement(groupmap.comment, ldb.FLAG_MOD_ADD, + 'description') + + # Fix up incorrect 'well known' groups that are actually builtin (per test above) to be aliases + if groupmap.sid_name_use == lsa.SID_NAME_ALIAS or groupmap.sid_name_use == lsa.SID_NAME_WKN_GRP: + m['groupType'] = ldb.MessageElement(str(dsdb.GTYPE_SECURITY_DOMAIN_LOCAL_GROUP), + ldb.FLAG_MOD_ADD, 'groupType') + + try: + samdb.add(m, controls=["relax:0"]) + except ldb.LdbError, e: + logger.warn('Could not add group name=%s (%s)', groupmap.nt_name, str(e)) + + +def add_users_to_group(samdb, group, members, logger): + """Add user/member to group/alias + + param samdb: Samba4 SAM database + param group: Groupmap object + param members: List of member SIDs + param logger: Logger object + """ + for member_sid in members: + m = ldb.Message() + m.dn = ldb.Dn(samdb, "<SID=%s>" % str(group.sid)) + m['a01'] = ldb.MessageElement("<SID=%s>" % str(member_sid), ldb.FLAG_MOD_ADD, 'member') + + try: + samdb.modify(m) + except ldb.LdbError, (ecode, emsg): + if ecode == ldb.ERR_ENTRY_ALREADY_EXISTS: + logger.debug("skipped re-adding member '%s' to group '%s': %s", member_sid, group.sid, emsg) + elif ecode == ldb.ERR_NO_SUCH_OBJECT: + raise ProvisioningError("Could not add member '%s' to group '%s' as either group or user record doesn't exist: %s" % (member_sid, group.sid, emsg)) + else: + raise ProvisioningError("Could not add member '%s' to group '%s': %s" % (member_sid, group.sid, emsg)) + + +def import_wins(samba4_winsdb, samba3_winsdb): + """Import settings from a Samba3 WINS database. + + :param samba4_winsdb: WINS database to import to + :param samba3_winsdb: WINS database to import from + """ + + version_id = 0 + + for (name, (ttl, ips, nb_flags)) in samba3_winsdb.items(): + version_id += 1 + + type = int(name.split("#", 1)[1], 16) + + if type == 0x1C: + rType = 0x2 + elif type & 0x80: + if len(ips) > 1: + rType = 0x2 + else: + rType = 0x1 + else: + if len(ips) > 1: + rType = 0x3 + else: + rType = 0x0 + + if ttl > time.time(): + rState = 0x0 # active + else: + rState = 0x1 # released + + nType = ((nb_flags & 0x60) >> 5) + + samba4_winsdb.add({"dn": "name=%s,type=0x%s" % tuple(name.split("#")), + "type": name.split("#")[1], + "name": name.split("#")[0], + "objectClass": "winsRecord", + "recordType": str(rType), + "recordState": str(rState), + "nodeType": str(nType), + "expireTime": ldb.timestring(ttl), + "isStatic": "0", + "versionID": str(version_id), + "address": ips}) + + samba4_winsdb.add({"dn": "cn=VERSION", + "cn": "VERSION", + "objectClass": "winsMaxVersion", + "maxVersion": str(version_id)}) + + +def enable_samba3sam(samdb, ldapurl): + """Enable Samba 3 LDAP URL database. + + :param samdb: SAM Database. + :param ldapurl: Samba 3 LDAP URL + """ + samdb.modify_ldif(""" +dn: @MODULES +changetype: modify +replace: @LIST +@LIST: samldb,operational,objectguid,rdn_name,samba3sam +""") + + samdb.add({"dn": "@MAP=samba3sam", "@MAP_URL": ldapurl}) + + +smbconf_keep = [ + "dos charset", + "unix charset", + "display charset", + "comment", + "path", + "directory", + "workgroup", + "realm", + "netbios name", + "netbios aliases", + "netbios scope", + "server string", + "interfaces", + "bind interfaces only", + "security", + "auth methods", + "encrypt passwords", + "null passwords", + "obey pam restrictions", + "password server", + "smb passwd file", + "private dir", + "passwd chat", + "password level", + "lanman auth", + "ntlm auth", + "client NTLMv2 auth", + "client lanman auth", + "client plaintext auth", + "read only", + "hosts allow", + "hosts deny", + "log level", + "debuglevel", + "log file", + "smb ports", + "large readwrite", + "max protocol", + "min protocol", + "unicode", + "read raw", + "write raw", + "disable netbios", + "nt status support", + "max mux", + "max xmit", + "name resolve order", + "max wins ttl", + "min wins ttl", + "time server", + "unix extensions", + "use spnego", + "server signing", + "client signing", + "max connections", + "paranoid server security", + "socket options", + "strict sync", + "max print jobs", + "printable", + "print ok", + "printer name", + "printer", + "map system", + "map hidden", + "map archive", + "preferred master", + "prefered master", + "local master", + "browseable", + "browsable", + "wins server", + "wins support", + "csc policy", + "strict locking", + "preload", + "auto services", + "lock dir", + "lock directory", + "pid directory", + "socket address", + "copy", + "include", + "available", + "volume", + "fstype", + "panic action", + "msdfs root", + "host msdfs", + "winbind separator"] + + +def upgrade_smbconf(oldconf, mark): + """Remove configuration variables not present in Samba4 + + :param oldconf: Old configuration structure + :param mark: Whether removed configuration variables should be + kept in the new configuration as "samba3:<name>" + """ + data = oldconf.data() + newconf = LoadParm() + + for s in data: + for p in data[s]: + keep = False + for k in smbconf_keep: + if smbconf_keep[k] == p: + keep = True + break + + if keep: + newconf.set(s, p, oldconf.get(s, p)) + elif mark: + newconf.set(s, "samba3:" + p, oldconf.get(s, p)) + + return newconf + +SAMBA3_PREDEF_NAMES = { + 'HKLM': registry.HKEY_LOCAL_MACHINE, +} + + +def import_registry(samba4_registry, samba3_regdb): + """Import a Samba 3 registry database into the Samba 4 registry. + + :param samba4_registry: Samba 4 registry handle. + :param samba3_regdb: Samba 3 registry database handle. + """ + def ensure_key_exists(keypath): + (predef_name, keypath) = keypath.split("/", 1) + predef_id = SAMBA3_PREDEF_NAMES[predef_name] + keypath = keypath.replace("/", "\\") + return samba4_registry.create_key(predef_id, keypath) + + for key in samba3_regdb.keys(): + key_handle = ensure_key_exists(key) + for subkey in samba3_regdb.subkeys(key): + ensure_key_exists(subkey) + for (value_name, (value_type, value_data)) in samba3_regdb.values(key).items(): + key_handle.set_value(value_name, value_type, value_data) + +def get_posix_attr_from_ldap_backend(logger, ldb_object, base_dn, user, attr): + """Get posix attributes from a samba3 ldap backend + :param ldbs: a list of ldb connection objects + :param base_dn: the base_dn of the connection + :param user: the user to get the attribute for + :param attr: the attribute to be retrieved + """ + try: + msg = ldb_object.search(base_dn, scope=ldb.SCOPE_SUBTREE, + expression=("(&(objectClass=posixAccount)(uid=%s))" + % (user)), attrs=[attr]) + except ldb.LdbError, e: + raise ProvisioningError("Failed to retrieve attribute %s for user %s, the error is: %s", attr, user, e) + else: + if msg.count <= 1: + # This will raise KeyError (which is what we want) if there isn't a entry for this user + return msg[0][attr][0] + else: + logger.warning("LDAP entry for user %s contains more than one %s", user, attr) + raise KeyError + + +def upgrade_from_samba3(samba3, logger, targetdir, session_info=None, + useeadb=False, dns_backend=None, use_ntvfs=False): + """Upgrade from samba3 database to samba4 AD database + + :param samba3: samba3 object + :param logger: Logger object + :param targetdir: samba4 database directory + :param session_info: Session information + """ + serverrole = samba3.lp.server_role() + + domainname = samba3.lp.get("workgroup") + realm = samba3.lp.get("realm") + netbiosname = samba3.lp.get("netbios name") + + if samba3.lp.get("ldapsam:trusted") is None: + samba3.lp.set("ldapsam:trusted", "yes") + + # secrets db + try: + secrets_db = samba3.get_secrets_db() + except IOError, e: + raise ProvisioningError("Could not open '%s', the Samba3 secrets database: %s. Perhaps you specified the incorrect smb.conf, --testparm or --dbdir option?" % (samba3.privatedir_path("secrets.tdb"), str(e))) + + if not domainname: + domainname = secrets_db.domains()[0] + logger.warning("No workgroup specified in smb.conf file, assuming '%s'", + domainname) + + if not realm: + if serverrole == "ROLE_DOMAIN_BDC" or serverrole == "ROLE_DOMAIN_PDC": + raise ProvisioningError("No realm specified in smb.conf file and being a DC. That upgrade path doesn't work! Please add a 'realm' directive to your old smb.conf to let us know which one you want to use (it is the DNS name of the AD domain you wish to create.") + else: + realm = domainname.upper() + logger.warning("No realm specified in smb.conf file, assuming '%s'", + realm) + + # Find machine account and password + next_rid = 1000 + + try: + machinepass = secrets_db.get_machine_password(netbiosname) + except KeyError: + machinepass = None + + if samba3.lp.get("passdb backend").split(":")[0].strip() == "ldapsam": + base_dn = samba3.lp.get("ldap suffix") + ldapuser = samba3.lp.get("ldap admin dn") + ldappass = (secrets_db.get_ldap_bind_pw(ldapuser)).strip('\x00') + ldap = True + else: + ldapuser = None + ldappass = None + ldap = False + + # We must close the direct pytdb database before the C code loads it + secrets_db.close() + + # Connect to old password backend + passdb.set_secrets_dir(samba3.lp.get("private dir")) + s3db = samba3.get_sam_db() + + # Get domain sid + try: + domainsid = passdb.get_global_sam_sid() + except passdb.error: + raise Exception("Can't find domain sid for '%s', Exiting." % domainname) + + # Get machine account, sid, rid + try: + machineacct = s3db.getsampwnam('%s$' % netbiosname) + except passdb.error: + machinerid = None + machinesid = None + else: + machinesid, machinerid = machineacct.user_sid.split() + + # Export account policy + logger.info("Exporting account policy") + policy = s3db.get_account_policy() + + # Export groups from old passdb backend + logger.info("Exporting groups") + grouplist = s3db.enum_group_mapping() + groupmembers = {} + for group in grouplist: + sid, rid = group.sid.split() + if sid == domainsid: + if rid >= next_rid: + next_rid = rid + 1 + + # Get members for each group/alias + if group.sid_name_use == lsa.SID_NAME_ALIAS: + try: + members = s3db.enum_aliasmem(group.sid) + groupmembers[str(group.sid)] = members + except passdb.error, e: + logger.warn("Ignoring group '%s' %s listed but then not found: %s", + group.nt_name, group.sid, e) + continue + elif group.sid_name_use == lsa.SID_NAME_DOM_GRP: + try: + members = s3db.enum_group_members(group.sid) + groupmembers[str(group.sid)] = members + except passdb.error, e: + logger.warn("Ignoring group '%s' %s listed but then not found: %s", + group.nt_name, group.sid, e) + continue + elif group.sid_name_use == lsa.SID_NAME_WKN_GRP: + (group_dom_sid, rid) = group.sid.split() + if (group_dom_sid != security.dom_sid(security.SID_BUILTIN)): + logger.warn("Ignoring 'well known' group '%s' (should already be in AD, and have no members)", + group.nt_name) + continue + # A number of buggy databases mix up well known groups and aliases. + try: + members = s3db.enum_aliasmem(group.sid) + groupmembers[str(group.sid)] = members + except passdb.error, e: + logger.warn("Ignoring group '%s' %s listed but then not found: %s", + group.nt_name, group.sid, e) + continue + else: + logger.warn("Ignoring group '%s' %s with sid_name_use=%d", + group.nt_name, group.sid, group.sid_name_use) + continue + + # Export users from old passdb backend + logger.info("Exporting users") + userlist = s3db.search_users(0) + userdata = {} + uids = {} + admin_user = None + for entry in userlist: + if machinerid and machinerid == entry['rid']: + continue + username = entry['account_name'] + if entry['rid'] < 1000: + logger.info(" Skipping wellknown rid=%d (for username=%s)", entry['rid'], username) + continue + if entry['rid'] >= next_rid: + next_rid = entry['rid'] + 1 + + user = s3db.getsampwnam(username) + acct_type = (user.acct_ctrl & (samr.ACB_NORMAL|samr.ACB_WSTRUST|samr.ACB_SVRTRUST|samr.ACB_DOMTRUST)) + if (acct_type == samr.ACB_NORMAL or acct_type == samr.ACB_WSTRUST): + pass + + elif acct_type == samr.ACB_SVRTRUST: + logger.warn(" Demoting BDC account trust for %s, this DC must be elevated to an AD DC using 'samba-tool domain promote'" % username[:-1]) + user.acct_ctrl = (user.acct_ctrl & ~samr.ACB_SVRTRUST) | samr.ACB_WSTRUST + + elif acct_type == samr.ACB_DOMTRUST: + logger.warn(" Skipping inter-domain trust from domain %s, this trust must be re-created as an AD trust" % username[:-1]) + + elif acct_type == (samr.ACB_NORMAL|samr.ACB_WSTRUST) and username[-1] == '$': + logger.warn(" Fixing account %s which had both ACB_NORMAL (U) and ACB_WSTRUST (W) set. Account will be marked as ACB_WSTRUST (W), i.e. as a domain member" % username) + user.acct_ctrl = (user.acct_ctrl & ~samr.ACB_NORMAL) + + elif acct_type == (samr.ACB_NORMAL|samr.ACB_SVRTRUST) and username[-1] == '$': + logger.warn(" Fixing account %s which had both ACB_NORMAL (U) and ACB_SVRTRUST (S) set. Account will be marked as ACB_WSTRUST (S), i.e. as a domain member" % username) + user.acct_ctrl = (user.acct_ctrl & ~samr.ACB_NORMAL) + + else: + raise ProvisioningError("""Failed to upgrade due to invalid account %s, account control flags 0x%08X must have exactly one of +ACB_NORMAL (N, 0x%08X), ACB_WSTRUST (W 0x%08X), ACB_SVRTRUST (S 0x%08X) or ACB_DOMTRUST (D 0x%08X). + +Please fix this account before attempting to upgrade again +""" + % (user.acct_flags, username, + samr.ACB_NORMAL, samr.ACB_WSTRUST, samr.ACB_SVRTRUST, samr.ACB_DOMTRUST)) + + userdata[username] = user + try: + uids[username] = s3db.sid_to_id(user.user_sid)[0] + except passdb.error: + try: + uids[username] = pwd.getpwnam(username).pw_uid + except KeyError: + pass + + if not admin_user and username.lower() == 'root': + admin_user = username + if username.lower() == 'administrator': + admin_user = username + + try: + group_memberships = s3db.enum_group_memberships(user); + for group in group_memberships: + if str(group) in groupmembers: + if user.user_sid not in groupmembers[str(group)]: + groupmembers[str(group)].append(user.user_sid) + else: + groupmembers[str(group)] = [user.user_sid]; + except passdb.error, e: + logger.warn("Ignoring group memberships of '%s' %s: %s", + username, user.user_sid, e) + + logger.info("Next rid = %d", next_rid) + + # Check for same username/groupname + group_names = set([g.nt_name for g in grouplist]) + user_names = set([u['account_name'] for u in userlist]) + common_names = group_names.intersection(user_names) + if common_names: + logger.error("Following names are both user names and group names:") + for name in common_names: + logger.error(" %s" % name) + raise ProvisioningError("Please remove common user/group names before upgrade.") + + # Check for same user sid/group sid + group_sids = set([str(g.sid) for g in grouplist]) + if len(grouplist) != len(group_sids): + raise ProvisioningError("Please remove duplicate group sid entries before upgrade.") + user_sids = set(["%s-%u" % (domainsid, u['rid']) for u in userlist]) + if len(userlist) != len(user_sids): + raise ProvisioningError("Please remove duplicate user sid entries before upgrade.") + common_sids = group_sids.intersection(user_sids) + if common_sids: + logger.error("Following sids are both user and group sids:") + for sid in common_sids: + logger.error(" %s" % str(sid)) + raise ProvisioningError("Please remove duplicate sid entries before upgrade.") + + # Get posix attributes from ldap or the os + homes = {} + shells = {} + pgids = {} + if ldap: + creds = Credentials() + creds.guess(samba3.lp) + creds.set_bind_dn(ldapuser) + creds.set_password(ldappass) + urls = samba3.lp.get("passdb backend").split(":",1)[1].strip('"') + for url in urls.split(): + try: + ldb_object = Ldb(url, credentials=creds) + except ldb.LdbError, e: + logger.warning("Could not open ldb connection to %s, the error message is: %s", url, e) + else: + break + logger.info("Exporting posix attributes") + userlist = s3db.search_users(0) + for entry in userlist: + username = entry['account_name'] + if username in uids.keys(): + try: + if ldap: + homes[username] = get_posix_attr_from_ldap_backend(logger, ldb_object, base_dn, username, "homeDirectory") + else: + homes[username] = pwd.getpwnam(username).pw_dir + except KeyError: + pass + except IndexError: + pass + + try: + if ldap: + shells[username] = get_posix_attr_from_ldap_backend(logger, ldb_object, base_dn, username, "loginShell") + else: + shells[username] = pwd.getpwnam(username).pw_shell + except KeyError: + pass + except IndexError: + pass + + try: + if ldap: + pgids[username] = get_posix_attr_from_ldap_backend(logger, ldb_object, base_dn, username, "gidNumber") + else: + pgids[username] = pwd.getpwnam(username).pw_gid + except KeyError: + pass + except IndexError: + pass + + logger.info("Reading WINS database") + samba3_winsdb = None + try: + samba3_winsdb = samba3.get_wins_db() + except IOError, e: + logger.warn('Cannot open wins database, Ignoring: %s', str(e)) + + if not (serverrole == "ROLE_DOMAIN_BDC" or serverrole == "ROLE_DOMAIN_PDC"): + dns_backend = "NONE" + + # If we found an admin user, set a fake pw that we will override. + # This avoids us printing out an admin password that we won't actually + # set. + if admin_user: + adminpass = generate_random_password(12, 32) + else: + adminpass = None + + # Do full provision + result = provision(logger, session_info, None, + targetdir=targetdir, realm=realm, domain=domainname, + domainsid=str(domainsid), next_rid=next_rid, + dc_rid=machinerid, adminpass = adminpass, + dom_for_fun_level=dsdb.DS_DOMAIN_FUNCTION_2003, + hostname=netbiosname.lower(), machinepass=machinepass, + serverrole=serverrole, samdb_fill=FILL_FULL, + useeadb=useeadb, dns_backend=dns_backend, use_rfc2307=True, + use_ntvfs=use_ntvfs, skip_sysvolacl=True) + result.report_logger(logger) + + # Import WINS database + logger.info("Importing WINS database") + + if samba3_winsdb: + import_wins(Ldb(result.paths.winsdb), samba3_winsdb) + + # Set Account policy + logger.info("Importing Account policy") + import_sam_policy(result.samdb, policy, logger) + + # Migrate IDMAP database + logger.info("Importing idmap database") + import_idmap(result.idmap, samba3, logger) + + # Set the s3 context for samba4 configuration + new_lp_ctx = s3param.get_context() + new_lp_ctx.load(result.lp.configfile) + new_lp_ctx.set("private dir", result.lp.get("private dir")) + new_lp_ctx.set("state directory", result.lp.get("state directory")) + new_lp_ctx.set("lock directory", result.lp.get("lock directory")) + + # Connect to samba4 backend + s4_passdb = passdb.PDB(new_lp_ctx.get("passdb backend")) + + # Export groups to samba4 backend + logger.info("Importing groups") + for g in grouplist: + # Ignore uninitialized groups (gid = -1) + if g.gid != -1: + add_group_from_mapping_entry(result.samdb, g, logger) + add_ad_posix_idmap_entry(result.samdb, g.sid, g.gid, "ID_TYPE_GID", logger) + add_posix_attrs(samdb=result.samdb, sid=g.sid, name=g.nt_name, nisdomain=domainname.lower(), xid_type="ID_TYPE_GID", logger=logger) + + # Export users to samba4 backend + logger.info("Importing users") + for username in userdata: + if username.lower() == 'administrator': + if userdata[username].user_sid != dom_sid(str(domainsid) + "-500"): + logger.error("User 'Administrator' in your existing directory has SID %s, expected it to be %s" % (userdata[username].user_sid, dom_sid(str(domainsid) + "-500"))) + raise ProvisioningError("User 'Administrator' in your existing directory does not have SID ending in -500") + if username.lower() == 'root': + if userdata[username].user_sid == dom_sid(str(domainsid) + "-500"): + logger.warn('User root has been replaced by Administrator') + else: + logger.warn('User root has been kept in the directory, it should be removed in favour of the Administrator user') + + s4_passdb.add_sam_account(userdata[username]) + if username in uids: + add_ad_posix_idmap_entry(result.samdb, userdata[username].user_sid, uids[username], "ID_TYPE_UID", logger) + if (username in homes) and (homes[username] is not None) and \ + (username in shells) and (shells[username] is not None) and \ + (username in pgids) and (pgids[username] is not None): + add_posix_attrs(samdb=result.samdb, sid=userdata[username].user_sid, name=username, nisdomain=domainname.lower(), xid_type="ID_TYPE_UID", home=homes[username], shell=shells[username], pgid=pgids[username], logger=logger) + + logger.info("Adding users to groups") + for g in grouplist: + if str(g.sid) in groupmembers: + add_users_to_group(result.samdb, g, groupmembers[str(g.sid)], logger) + + # Set password for administrator + if admin_user: + logger.info("Setting password for administrator") + admin_userdata = s4_passdb.getsampwnam("administrator") + admin_userdata.nt_passwd = userdata[admin_user].nt_passwd + if userdata[admin_user].lanman_passwd: + admin_userdata.lanman_passwd = userdata[admin_user].lanman_passwd + admin_userdata.pass_last_set_time = userdata[admin_user].pass_last_set_time + if userdata[admin_user].pw_history: + admin_userdata.pw_history = userdata[admin_user].pw_history + s4_passdb.update_sam_account(admin_userdata) + logger.info("Administrator password has been set to password of user '%s'", admin_user) + + if result.server_role == "active directory domain controller": + setsysvolacl(result.samdb, result.paths.netlogon, result.paths.sysvol, + result.paths.root_uid, result.paths.root_gid, + security.dom_sid(result.domainsid), result.names.dnsdomain, + result.names.domaindn, result.lp, use_ntvfs) + + # FIXME: import_registry(registry.Registry(), samba3.get_registry()) + # FIXME: shares diff --git a/python/samba/upgradehelpers.py b/python/samba/upgradehelpers.py new file mode 100644 index 00000000000..1ec19d4ab61 --- /dev/null +++ b/python/samba/upgradehelpers.py @@ -0,0 +1,913 @@ +# Helpers for provision stuff +# Copyright (C) Matthieu Patou <mat@matws.net> 2009-2012 +# +# Based on provision a Samba4 server by +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2008 +# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2008 +# +# +# 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/>. + +"""Helpers used for upgrading between different database formats.""" + +import os +import re +import shutil +import samba + +from samba import Ldb, version, ntacls +from ldb import SCOPE_SUBTREE, SCOPE_ONELEVEL, SCOPE_BASE +import ldb +from samba.provision import (provision_paths_from_lp, + getpolicypath, set_gpos_acl, create_gpo_struct, + FILL_FULL, provision, ProvisioningError, + setsysvolacl, secretsdb_self_join) +from samba.dcerpc import xattr, drsblobs +from samba.dcerpc.misc import SEC_CHAN_BDC +from samba.ndr import ndr_unpack +from samba.samdb import SamDB +from samba import _glue +import tempfile + +# All the ldb related to registry are commented because the path for them is +# relative in the provisionPath object +# And so opening them create a file in the current directory which is not what +# we want +# I still keep them commented because I plan soon to make more cleaner +ERROR = -1 +SIMPLE = 0x00 +CHANGE = 0x01 +CHANGESD = 0x02 +GUESS = 0x04 +PROVISION = 0x08 +CHANGEALL = 0xff + +hashAttrNotCopied = set(["dn", "whenCreated", "whenChanged", "objectGUID", + "uSNCreated", "replPropertyMetaData", "uSNChanged", "parentGUID", + "objectCategory", "distinguishedName", "nTMixedDomain", + "showInAdvancedViewOnly", "instanceType", "msDS-Behavior-Version", + "nextRid", "cn", "versionNumber", "lmPwdHistory", "pwdLastSet", + "ntPwdHistory", "unicodePwd","dBCSPwd", "supplementalCredentials", + "gPCUserExtensionNames", "gPCMachineExtensionNames","maxPwdAge", "secret", + "possibleInferiors", "privilege", "sAMAccountType"]) + + +class ProvisionLDB(object): + + def __init__(self): + self.sam = None + self.secrets = None + self.idmap = None + self.privilege = None + self.hkcr = None + self.hkcu = None + self.hku = None + self.hklm = None + + def dbs(self): + return (self.sam, self.secrets, self.idmap, self.privilege) + + def startTransactions(self): + for db in self.dbs(): + db.transaction_start() +# TO BE DONE +# self.hkcr.transaction_start() +# self.hkcu.transaction_start() +# self.hku.transaction_start() +# self.hklm.transaction_start() + + def groupedRollback(self): + ok = True + for db in self.dbs(): + try: + db.transaction_cancel() + except Exception: + ok = False + return ok +# TO BE DONE +# self.hkcr.transaction_cancel() +# self.hkcu.transaction_cancel() +# self.hku.transaction_cancel() +# self.hklm.transaction_cancel() + + def groupedCommit(self): + try: + for db in self.dbs(): + db.transaction_prepare_commit() + except Exception: + return self.groupedRollback() +# TO BE DONE +# self.hkcr.transaction_prepare_commit() +# self.hkcu.transaction_prepare_commit() +# self.hku.transaction_prepare_commit() +# self.hklm.transaction_prepare_commit() + try: + for db in self.dbs(): + db.transaction_commit() + except Exception: + return self.groupedRollback() + +# TO BE DONE +# self.hkcr.transaction_commit() +# self.hkcu.transaction_commit() +# self.hku.transaction_commit() +# self.hklm.transaction_commit() + return True + + +def get_ldbs(paths, creds, session, lp): + """Return LDB object mapped on most important databases + + :param paths: An object holding the different importants paths for provision object + :param creds: Credential used for openning LDB files + :param session: Session to use for openning LDB files + :param lp: A loadparam object + :return: A ProvisionLDB object that contains LDB object for the different LDB files of the provision""" + + ldbs = ProvisionLDB() + + ldbs.sam = SamDB(paths.samdb, session_info=session, credentials=creds, lp=lp, options=["modules:samba_dsdb"]) + ldbs.secrets = Ldb(paths.secrets, session_info=session, credentials=creds, lp=lp) + ldbs.idmap = Ldb(paths.idmapdb, session_info=session, credentials=creds, lp=lp) + ldbs.privilege = Ldb(paths.privilege, session_info=session, credentials=creds, lp=lp) +# ldbs.hkcr = Ldb(paths.hkcr, session_info=session, credentials=creds, lp=lp) +# ldbs.hkcu = Ldb(paths.hkcu, session_info=session, credentials=creds, lp=lp) +# ldbs.hku = Ldb(paths.hku, session_info=session, credentials=creds, lp=lp) +# ldbs.hklm = Ldb(paths.hklm, session_info=session, credentials=creds, lp=lp) + + return ldbs + + +def usn_in_range(usn, range): + """Check if the usn is in one of the range provided. + To do so, the value is checked to be between the lower bound and + higher bound of a range + + :param usn: A integer value corresponding to the usn that we want to update + :param range: A list of integer representing ranges, lower bounds are in + the even indices, higher in odd indices + :return: True if the usn is in one of the range, False otherwise + """ + + idx = 0 + cont = True + ok = False + while cont: + if idx == len(range): + cont = False + continue + if usn < int(range[idx]): + if idx %2 == 1: + ok = True + cont = False + if usn == int(range[idx]): + cont = False + ok = True + idx = idx + 1 + return ok + + +def get_paths(param, targetdir=None, smbconf=None): + """Get paths to important provision objects (smb.conf, ldb files, ...) + + :param param: Param object + :param targetdir: Directory where the provision is (or will be) stored + :param smbconf: Path to the smb.conf file + :return: A list with the path of important provision objects""" + if targetdir is not None: + if not os.path.exists(targetdir): + os.mkdir(targetdir) + etcdir = os.path.join(targetdir, "etc") + if not os.path.exists(etcdir): + os.makedirs(etcdir) + smbconf = os.path.join(etcdir, "smb.conf") + if smbconf is None: + smbconf = param.default_path() + + if not os.path.exists(smbconf): + raise ProvisioningError("Unable to find smb.conf") + + lp = param.LoadParm() + lp.load(smbconf) + paths = provision_paths_from_lp(lp, lp.get("realm")) + return paths + +def update_policyids(names, samdb): + """Update policy ids that could have changed after sam update + + :param names: List of key provision parameters + :param samdb: An Ldb object conntected with the sam DB + """ + # policy guid + res = samdb.search(expression="(displayName=Default Domain Policy)", + base="CN=Policies,CN=System," + str(names.rootdn), + scope=SCOPE_ONELEVEL, attrs=["cn","displayName"]) + names.policyid = str(res[0]["cn"]).replace("{","").replace("}","") + # dc policy guid + res2 = samdb.search(expression="(displayName=Default Domain Controllers" + " Policy)", + base="CN=Policies,CN=System," + str(names.rootdn), + scope=SCOPE_ONELEVEL, attrs=["cn","displayName"]) + if len(res2) == 1: + names.policyid_dc = str(res2[0]["cn"]).replace("{","").replace("}","") + else: + names.policyid_dc = None + + +def newprovision(names, creds, session, smbconf, provdir, logger): + """Create a new provision. + + This provision will be the reference for knowing what has changed in the + since the latest upgrade in the current provision + + :param names: List of provision parameters + :param creds: Credentials for the authentification + :param session: Session object + :param smbconf: Path to the smb.conf file + :param provdir: Directory where the provision will be stored + :param logger: A Logger + """ + if os.path.isdir(provdir): + shutil.rmtree(provdir) + os.mkdir(provdir) + logger.info("Provision stored in %s", provdir) + return provision(logger, session, creds, smbconf=smbconf, + targetdir=provdir, samdb_fill=FILL_FULL, realm=names.realm, + domain=names.domain, domainguid=names.domainguid, + domainsid=str(names.domainsid), ntdsguid=names.ntdsguid, + policyguid=names.policyid, policyguid_dc=names.policyid_dc, + hostname=names.netbiosname.lower(), hostip=None, hostip6=None, + invocationid=names.invocation, adminpass=names.adminpass, + krbtgtpass=None, machinepass=None, dnspass=None, root=None, + nobody=None, users=None, + serverrole="domain controller", + backend_type=None, ldapadminpass=None, ol_mmr_urls=None, + slapd_path=None, + dom_for_fun_level=names.domainlevel, dns_backend=names.dns_backend, + useeadb=True, use_ntvfs=True) + + +def dn_sort(x, y): + """Sorts two DNs in the lexicographical order it and put higher level DN + before. + + So given the dns cn=bar,cn=foo and cn=foo the later will be return as + smaller + + :param x: First object to compare + :param y: Second object to compare + """ + p = re.compile(r'(?<!\\), ?') + tab1 = p.split(str(x)) + tab2 = p.split(str(y)) + minimum = min(len(tab1), len(tab2)) + len1 = len(tab1)-1 + len2 = len(tab2)-1 + # Note: python range go up to upper limit but do not include it + for i in range(0, minimum): + ret = cmp(tab1[len1-i], tab2[len2-i]) + if ret != 0: + return ret + else: + if i == minimum-1: + assert len1!=len2,"PB PB PB" + " ".join(tab1)+" / " + " ".join(tab2) + if len1 > len2: + return 1 + else: + return -1 + return ret + + +def identic_rename(ldbobj, dn): + """Perform a back and forth rename to trigger renaming on attribute that + can't be directly modified. + + :param lbdobj: An Ldb Object + :param dn: DN of the object to manipulate + """ + (before, after) = str(dn).split('=', 1) + # we need to use relax to avoid the subtree_rename constraints + ldbobj.rename(dn, ldb.Dn(ldbobj, "%s=foo%s" % (before, after)), ["relax:0"]) + ldbobj.rename(ldb.Dn(ldbobj, "%s=foo%s" % (before, after)), dn, ["relax:0"]) + + +def chunck_acl(acl): + """Return separate ACE of an ACL + + :param acl: A string representing the ACL + :return: A hash with different parts + """ + + p = re.compile(r'(\w+)?(\(.*?\))') + tab = p.findall(acl) + + hash = {} + hash["aces"] = [] + for e in tab: + if len(e[0]) > 0: + hash["flags"] = e[0] + hash["aces"].append(e[1]) + + return hash + + +def chunck_sddl(sddl): + """ Return separate parts of the SDDL (owner, group, ...) + + :param sddl: An string containing the SDDL to chunk + :return: A hash with the different chunk + """ + + p = re.compile(r'([OGDS]:)(.*?)(?=(?:[GDS]:|$))') + tab = p.findall(sddl) + + hash = {} + for e in tab: + if e[0] == "O:": + hash["owner"] = e[1] + if e[0] == "G:": + hash["group"] = e[1] + if e[0] == "D:": + hash["dacl"] = e[1] + if e[0] == "S:": + hash["sacl"] = e[1] + + return hash + + +def get_diff_sddls(refsddl, cursddl, checkSacl = True): + """Get the difference between 2 sddl + + This function split the textual representation of ACL into smaller + chunck in order to not to report a simple permutation as a difference + + :param refsddl: First sddl to compare + :param cursddl: Second sddl to compare + :param checkSacl: If false we skip the sacl checks + :return: A string that explain difference between sddls + """ + + txt = "" + hash_cur = chunck_sddl(cursddl) + hash_ref = chunck_sddl(refsddl) + + if not hash_cur.has_key("owner"): + txt = "\tNo owner in current SD" + elif hash_cur["owner"] != hash_ref["owner"]: + txt = "\tOwner mismatch: %s (in ref) %s" \ + "(in current)\n" % (hash_ref["owner"], hash_cur["owner"]) + + if not hash_cur.has_key("group"): + txt = "%s\tNo group in current SD" % txt + elif hash_cur["group"] != hash_ref["group"]: + txt = "%s\tGroup mismatch: %s (in ref) %s" \ + "(in current)\n" % (txt, hash_ref["group"], hash_cur["group"]) + + parts = [ "dacl" ] + if checkSacl: + parts.append("sacl") + for part in parts: + if hash_cur.has_key(part) and hash_ref.has_key(part): + + # both are present, check if they contain the same ACE + h_cur = set() + h_ref = set() + c_cur = chunck_acl(hash_cur[part]) + c_ref = chunck_acl(hash_ref[part]) + + for elem in c_cur["aces"]: + h_cur.add(elem) + + for elem in c_ref["aces"]: + h_ref.add(elem) + + for k in set(h_ref): + if k in h_cur: + h_cur.remove(k) + h_ref.remove(k) + + if len(h_cur) + len(h_ref) > 0: + txt = "%s\tPart %s is different between reference" \ + " and current here is the detail:\n" % (txt, part) + + for item in h_cur: + txt = "%s\t\t%s ACE is not present in the" \ + " reference\n" % (txt, item) + + for item in h_ref: + txt = "%s\t\t%s ACE is not present in the" \ + " current\n" % (txt, item) + + elif hash_cur.has_key(part) and not hash_ref.has_key(part): + txt = "%s\tReference ACL hasn't a %s part\n" % (txt, part) + elif not hash_cur.has_key(part) and hash_ref.has_key(part): + txt = "%s\tCurrent ACL hasn't a %s part\n" % (txt, part) + + return txt + + +def update_secrets(newsecrets_ldb, secrets_ldb, messagefunc): + """Update secrets.ldb + + :param newsecrets_ldb: An LDB object that is connected to the secrets.ldb + of the reference provision + :param secrets_ldb: An LDB object that is connected to the secrets.ldb + of the updated provision + """ + + messagefunc(SIMPLE, "Update of secrets.ldb") + reference = newsecrets_ldb.search(base="@MODULES", scope=SCOPE_BASE) + current = secrets_ldb.search(base="@MODULES", scope=SCOPE_BASE) + assert reference, "Reference modules list can not be empty" + if len(current) == 0: + # No modules present + delta = secrets_ldb.msg_diff(ldb.Message(), reference[0]) + delta.dn = reference[0].dn + secrets_ldb.add(reference[0]) + else: + delta = secrets_ldb.msg_diff(current[0], reference[0]) + delta.dn = current[0].dn + secrets_ldb.modify(delta) + + reference = newsecrets_ldb.search(expression="objectClass=top", base="", + scope=SCOPE_SUBTREE, attrs=["dn"]) + current = secrets_ldb.search(expression="objectClass=top", base="", + scope=SCOPE_SUBTREE, attrs=["dn"]) + hash_new = {} + hash = {} + listMissing = [] + listPresent = [] + + empty = ldb.Message() + for i in range(0, len(reference)): + hash_new[str(reference[i]["dn"]).lower()] = reference[i]["dn"] + + # Create a hash for speeding the search of existing object in the + # current provision + for i in range(0, len(current)): + hash[str(current[i]["dn"]).lower()] = current[i]["dn"] + + for k in hash_new.keys(): + if not hash.has_key(k): + listMissing.append(hash_new[k]) + else: + listPresent.append(hash_new[k]) + + for entry in listMissing: + reference = newsecrets_ldb.search(expression="distinguishedName=%s" % entry, + base="", scope=SCOPE_SUBTREE) + current = secrets_ldb.search(expression="distinguishedName=%s" % entry, + base="", scope=SCOPE_SUBTREE) + delta = secrets_ldb.msg_diff(empty, reference[0]) + for att in hashAttrNotCopied: + delta.remove(att) + messagefunc(CHANGE, "Entry %s is missing from secrets.ldb" % + reference[0].dn) + for att in delta: + messagefunc(CHANGE, " Adding attribute %s" % att) + delta.dn = reference[0].dn + secrets_ldb.add(delta) + + for entry in listPresent: + reference = newsecrets_ldb.search(expression="distinguishedName=%s" % entry, + base="", scope=SCOPE_SUBTREE) + current = secrets_ldb.search(expression="distinguishedName=%s" % entry, base="", + scope=SCOPE_SUBTREE) + delta = secrets_ldb.msg_diff(current[0], reference[0]) + for att in hashAttrNotCopied: + delta.remove(att) + for att in delta: + if att == "name": + messagefunc(CHANGE, "Found attribute name on %s," + " must rename the DN" % (current[0].dn)) + identic_rename(secrets_ldb, reference[0].dn) + else: + delta.remove(att) + + for entry in listPresent: + reference = newsecrets_ldb.search(expression="distinguishedName=%s" % entry, base="", + scope=SCOPE_SUBTREE) + current = secrets_ldb.search(expression="distinguishedName=%s" % entry, base="", + scope=SCOPE_SUBTREE) + delta = secrets_ldb.msg_diff(current[0], reference[0]) + for att in hashAttrNotCopied: + delta.remove(att) + for att in delta: + if att == "msDS-KeyVersionNumber": + delta.remove(att) + if att != "dn": + messagefunc(CHANGE, + "Adding/Changing attribute %s to %s" % + (att, current[0].dn)) + + delta.dn = current[0].dn + secrets_ldb.modify(delta) + + res2 = secrets_ldb.search(expression="(samaccountname=dns)", + scope=SCOPE_SUBTREE, attrs=["dn"]) + + if len(res2) == 1: + messagefunc(SIMPLE, "Remove old dns account") + secrets_ldb.delete(res2[0]["dn"]) + + +def getOEMInfo(samdb, rootdn): + """Return OEM Information on the top level Samba4 use to store version + info in this field + + :param samdb: An LDB object connect to sam.ldb + :param rootdn: Root DN of the domain + :return: The content of the field oEMInformation (if any) + """ + res = samdb.search(expression="(objectClass=*)", base=str(rootdn), + scope=SCOPE_BASE, attrs=["dn", "oEMInformation"]) + if len(res) > 0 and res[0].get("oEMInformation"): + info = res[0]["oEMInformation"] + return info + else: + return "" + + +def updateOEMInfo(samdb, rootdn): + """Update the OEMinfo field to add information about upgrade + + :param samdb: an LDB object connected to the sam DB + :param rootdn: The string representation of the root DN of + the provision (ie. DC=...,DC=...) + """ + res = samdb.search(expression="(objectClass=*)", base=rootdn, + scope=SCOPE_BASE, attrs=["dn", "oEMInformation"]) + if len(res) > 0: + if res[0].get("oEMInformation"): + info = str(res[0]["oEMInformation"]) + else: + info = "" + info = "%s, upgrade to %s" % (info, version) + delta = ldb.Message() + delta.dn = ldb.Dn(samdb, str(res[0]["dn"])) + delta["oEMInformation"] = ldb.MessageElement(info, ldb.FLAG_MOD_REPLACE, + "oEMInformation" ) + samdb.modify(delta) + +def update_gpo(paths, samdb, names, lp, message): + """Create missing GPO file object if needed + """ + dir = getpolicypath(paths.sysvol, names.dnsdomain, names.policyid) + if not os.path.isdir(dir): + create_gpo_struct(dir) + + if names.policyid_dc is None: + raise ProvisioningError("Policy ID for Domain controller is missing") + dir = getpolicypath(paths.sysvol, names.dnsdomain, names.policyid_dc) + if not os.path.isdir(dir): + create_gpo_struct(dir) + +def increment_calculated_keyversion_number(samdb, rootdn, hashDns): + """For a given hash associating dn and a number, this function will + update the replPropertyMetaData of each dn in the hash, so that the + calculated value of the msDs-KeyVersionNumber is equal or superior to the + one associated to the given dn. + + :param samdb: An SamDB object pointing to the sam + :param rootdn: The base DN where we want to start + :param hashDns: A hash with dn as key and number representing the + minimum value of msDs-KeyVersionNumber that we want to + have + """ + entry = samdb.search(expression='(objectClass=user)', + base=ldb.Dn(samdb,str(rootdn)), + scope=SCOPE_SUBTREE, attrs=["msDs-KeyVersionNumber"], + controls=["search_options:1:2"]) + done = 0 + hashDone = {} + if len(entry) == 0: + raise ProvisioningError("Unable to find msDs-KeyVersionNumber") + else: + for e in entry: + if hashDns.has_key(str(e.dn).lower()): + val = e.get("msDs-KeyVersionNumber") + if not val: + val = "0" + version = int(str(hashDns[str(e.dn).lower()])) + if int(str(val)) < version: + done = done + 1 + samdb.set_attribute_replmetadata_version(str(e.dn), + "unicodePwd", + version, True) +def delta_update_basesamdb(refsampath, sampath, creds, session, lp, message): + """Update the provision container db: sam.ldb + This function is aimed for alpha9 and newer; + + :param refsampath: Path to the samdb in the reference provision + :param sampath: Path to the samdb in the upgraded provision + :param creds: Credential used for openning LDB files + :param session: Session to use for openning LDB files + :param lp: A loadparam object + :return: A msg_diff object with the difference between the @ATTRIBUTES + of the current provision and the reference provision + """ + + message(SIMPLE, + "Update base samdb by searching difference with reference one") + refsam = Ldb(refsampath, session_info=session, credentials=creds, + lp=lp, options=["modules:"]) + sam = Ldb(sampath, session_info=session, credentials=creds, lp=lp, + options=["modules:"]) + + empty = ldb.Message() + deltaattr = None + reference = refsam.search(expression="") + + for refentry in reference: + entry = sam.search(expression="distinguishedName=%s" % refentry["dn"], + scope=SCOPE_SUBTREE) + if not len(entry): + delta = sam.msg_diff(empty, refentry) + message(CHANGE, "Adding %s to sam db" % str(refentry.dn)) + if str(refentry.dn) == "@PROVISION" and\ + delta.get(samba.provision.LAST_PROVISION_USN_ATTRIBUTE): + delta.remove(samba.provision.LAST_PROVISION_USN_ATTRIBUTE) + delta.dn = refentry.dn + sam.add(delta) + else: + delta = sam.msg_diff(entry[0], refentry) + if str(refentry.dn) == "@ATTRIBUTES": + deltaattr = sam.msg_diff(refentry, entry[0]) + if str(refentry.dn) == "@PROVISION" and\ + delta.get(samba.provision.LAST_PROVISION_USN_ATTRIBUTE): + delta.remove(samba.provision.LAST_PROVISION_USN_ATTRIBUTE) + if len(delta.items()) > 1: + delta.dn = refentry.dn + sam.modify(delta) + + return deltaattr + + +def construct_existor_expr(attrs): + """Construct a exists or LDAP search expression. + + :param attrs: List of attribute on which we want to create the search + expression. + :return: A string representing the expression, if attrs is empty an + empty string is returned + """ + expr = "" + if len(attrs) > 0: + expr = "(|" + for att in attrs: + expr = "%s(%s=*)"%(expr,att) + expr = "%s)"%expr + return expr + +def update_machine_account_password(samdb, secrets_ldb, names): + """Update (change) the password of the current DC both in the SAM db and in + secret one + + :param samdb: An LDB object related to the sam.ldb file of a given provision + :param secrets_ldb: An LDB object related to the secrets.ldb file of a given + provision + :param names: List of key provision parameters""" + + expression = "samAccountName=%s$" % names.netbiosname + secrets_msg = secrets_ldb.search(expression=expression, + attrs=["secureChannelType"]) + if int(secrets_msg[0]["secureChannelType"][0]) == SEC_CHAN_BDC: + res = samdb.search(expression=expression, attrs=[]) + assert(len(res) == 1) + + msg = ldb.Message(res[0].dn) + machinepass = samba.generate_random_password(128, 255) + mputf16 = machinepass.encode('utf-16-le') + msg["clearTextPassword"] = ldb.MessageElement(mputf16, + ldb.FLAG_MOD_REPLACE, + "clearTextPassword") + samdb.modify(msg) + + res = samdb.search(expression=("samAccountName=%s$" % names.netbiosname), + attrs=["msDs-keyVersionNumber"]) + assert(len(res) == 1) + kvno = int(str(res[0]["msDs-keyVersionNumber"])) + secChanType = int(secrets_msg[0]["secureChannelType"][0]) + + secretsdb_self_join(secrets_ldb, domain=names.domain, + realm=names.realm, + domainsid=names.domainsid, + dnsdomain=names.dnsdomain, + netbiosname=names.netbiosname, + machinepass=machinepass, + key_version_number=kvno, + secure_channel_type=secChanType) + else: + raise ProvisioningError("Unable to find a Secure Channel" + "of type SEC_CHAN_BDC") + +def update_dns_account_password(samdb, secrets_ldb, names): + """Update (change) the password of the dns both in the SAM db and in + secret one + + :param samdb: An LDB object related to the sam.ldb file of a given provision + :param secrets_ldb: An LDB object related to the secrets.ldb file of a given + provision + :param names: List of key provision parameters""" + + expression = "samAccountName=dns-%s" % names.netbiosname + secrets_msg = secrets_ldb.search(expression=expression) + if len(secrets_msg) == 1: + res = samdb.search(expression=expression, attrs=[]) + assert(len(res) == 1) + + msg = ldb.Message(res[0].dn) + machinepass = samba.generate_random_password(128, 255) + mputf16 = machinepass.encode('utf-16-le') + msg["clearTextPassword"] = ldb.MessageElement(mputf16, + ldb.FLAG_MOD_REPLACE, + "clearTextPassword") + + samdb.modify(msg) + + res = samdb.search(expression=expression, + attrs=["msDs-keyVersionNumber"]) + assert(len(res) == 1) + kvno = str(res[0]["msDs-keyVersionNumber"]) + + msg = ldb.Message(secrets_msg[0].dn) + msg["secret"] = ldb.MessageElement(machinepass, + ldb.FLAG_MOD_REPLACE, + "secret") + msg["msDS-KeyVersionNumber"] = ldb.MessageElement(kvno, + ldb.FLAG_MOD_REPLACE, + "msDS-KeyVersionNumber") + + secrets_ldb.modify(msg) + +def search_constructed_attrs_stored(samdb, rootdn, attrs): + """Search a given sam DB for calculated attributes that are + still stored in the db. + + :param samdb: An LDB object pointing to the sam + :param rootdn: The base DN where the search should start + :param attrs: A list of attributes to be searched + :return: A hash with attributes as key and an array of + array. Each array contains the dn and the associated + values for this attribute as they are stored in the + sam.""" + + hashAtt = {} + expr = construct_existor_expr(attrs) + if expr == "": + return hashAtt + entry = samdb.search(expression=expr, base=ldb.Dn(samdb, str(rootdn)), + scope=SCOPE_SUBTREE, attrs=attrs, + controls=["search_options:1:2","bypassoperational:0"]) + if len(entry) == 0: + # Nothing anymore + return hashAtt + + for ent in entry: + for att in attrs: + if ent.get(att): + if hashAtt.has_key(att): + hashAtt[att][str(ent.dn).lower()] = str(ent[att]) + else: + hashAtt[att] = {} + hashAtt[att][str(ent.dn).lower()] = str(ent[att]) + + return hashAtt + +def findprovisionrange(samdb, basedn): + """ Find ranges of usn grouped by invocation id and then by timestamp + rouned at 1 minute + + :param samdb: An LDB object pointing to the samdb + :param basedn: The DN of the forest + + :return: A two level dictionary with invoication id as the + first level, timestamp as the second one and then + max, min, and number as subkeys, representing respectivily + the maximum usn for the range, the minimum usn and the number + of object with usn in this range. + """ + nb_obj = 0 + hash_id = {} + + res = samdb.search(base=basedn, expression="objectClass=*", + scope=ldb.SCOPE_SUBTREE, + attrs=["replPropertyMetaData"], + controls=["search_options:1:2"]) + + for e in res: + nb_obj = nb_obj + 1 + obj = ndr_unpack(drsblobs.replPropertyMetaDataBlob, + str(e["replPropertyMetaData"])).ctr + + for o in obj.array: + # like a timestamp but with the resolution of 1 minute + minutestamp =_glue.nttime2unix(o.originating_change_time)/60 + hash_ts = hash_id.get(str(o.originating_invocation_id)) + + if hash_ts is None: + ob = {} + ob["min"] = o.originating_usn + ob["max"] = o.originating_usn + ob["num"] = 1 + ob["list"] = [str(e.dn)] + hash_ts = {} + else: + ob = hash_ts.get(minutestamp) + if ob is None: + ob = {} + ob["min"] = o.originating_usn + ob["max"] = o.originating_usn + ob["num"] = 1 + ob["list"] = [str(e.dn)] + else: + if ob["min"] > o.originating_usn: + ob["min"] = o.originating_usn + if ob["max"] < o.originating_usn: + ob["max"] = o.originating_usn + if not (str(e.dn) in ob["list"]): + ob["num"] = ob["num"] + 1 + ob["list"].append(str(e.dn)) + hash_ts[minutestamp] = ob + hash_id[str(o.originating_invocation_id)] = hash_ts + + return (hash_id, nb_obj) + +def print_provision_ranges(dic, limit_print, dest, samdb_path, invocationid): + """ print the differents ranges passed as parameter + + :param dic: A dictionnary as returned by findprovisionrange + :param limit_print: minimum number of object in a range in order to print it + :param dest: Destination directory + :param samdb_path: Path to the sam.ldb file + :param invoicationid: Invocation ID for the current provision + """ + ldif = "" + + for id in dic: + hash_ts = dic[id] + sorted_keys = [] + sorted_keys.extend(hash_ts.keys()) + sorted_keys.sort() + + kept_record = [] + for k in sorted_keys: + obj = hash_ts[k] + if obj["num"] > limit_print: + dt = _glue.nttime2string(_glue.unix2nttime(k*60)) + print "%s # of modification: %d \tmin: %d max: %d" % (dt , obj["num"], + obj["min"], + obj["max"]) + if hash_ts[k]["num"] > 600: + kept_record.append(k) + + # Let's try to concatenate consecutive block if they are in the almost same minutestamp + for i in range(0, len(kept_record)): + if i != 0: + key1 = kept_record[i] + key2 = kept_record[i-1] + if key1 - key2 == 1: + # previous record is just 1 minute away from current + if int(hash_ts[key1]["min"]) == int(hash_ts[key2]["max"]) + 1: + # Copy the highest USN in the previous record + # and mark the current as skipped + hash_ts[key2]["max"] = hash_ts[key1]["max"] + hash_ts[key1]["skipped"] = True + + for k in kept_record: + obj = hash_ts[k] + if obj.get("skipped") is None: + ldif = "%slastProvisionUSN: %d-%d;%s\n" % (ldif, obj["min"], + obj["max"], id) + + if ldif != "": + file = tempfile.mktemp(dir=dest, prefix="usnprov", suffix=".ldif") + print + print "To track the USNs modified/created by provision and upgrade proivsion," + print " the following ranges are proposed to be added to your provision sam.ldb: \n%s" % ldif + print "We recommend to review them, and if it's correct to integrate the following ldif: %s in your sam.ldb" % file + print "You can load this file like this: ldbadd -H %s %s\n"%(str(samdb_path),file) + ldif = "dn: @PROVISION\nprovisionnerID: %s\n%s" % (invocationid, ldif) + open(file,'w').write(ldif) + +def int64range2str(value): + """Display the int64 range stored in value as xxx-yyy + + :param value: The int64 range + :return: A string of the representation of the range + """ + + lvalue = long(value) + str = "%d-%d" % (lvalue&0xFFFFFFFF, lvalue>>32) + return str diff --git a/python/samba/web_server/__init__.py b/python/samba/web_server/__init__.py new file mode 100644 index 00000000000..78ce953c612 --- /dev/null +++ b/python/samba/web_server/__init__.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# +# Unix SMB/CIFS implementation. +# Copyright © Jelmer Vernooij <jelmer@samba.org> 2008 +# +# Implementation of SWAT that uses WSGI +# +# 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/>. +# + +def render_placeholder(environ, start_response): + """Send the user a simple placeholder about missing SWAT.""" + status = '200 OK' + response_headers = [('Content-type', 'text/html')] + start_response(status, response_headers) + + yield "<!doctype html>\n" + yield "<html>\n" + yield " <title>The Samba web service</title>\n" + yield "</html>\n" + + yield "<body>\n" + yield "<p>Welcome to this Samba web server.</p>\n" + yield "<p>This page is a simple placeholder. You probably want to install " + yield "SWAT. More information can be found " + yield "<a href='http://wiki.samba.org/index.php/SWAT2'>on the wiki</a>.</p>" + yield "</p>\n" + yield "</body>\n" + yield "</html>\n" + + +def __call__(environ, start_response): + """Handle a HTTP request.""" + from wsgiref.util import application_uri, shift_path_info + from urlparse import urljoin + + try: + import swat + except ImportError, e: + print "NO SWAT: %r" % e + have_swat = False + else: + have_swat = True + + orig_path = environ['PATH_INFO'] + name = shift_path_info(environ) + + if name == "": + if have_swat: + start_response('301 Redirect', + [('Location', urljoin(application_uri(environ), 'swat')),]) + return [] + else: + return render_placeholder(environ, start_response) + elif have_swat and name == "swat": + return swat.__call__(environ, start_response) + else: + status = '404 Not found' + response_headers = [('Content-type', 'text/html')] + start_response(status, response_headers) + return ["The path %s (%s) was not found" % (orig_path, name)] + + +if __name__ == '__main__': + from wsgiref import simple_server + httpd = simple_server.make_server('localhost', 8090, __call__) + print "Serving HTTP on port 8090..." + httpd.serve_forever() diff --git a/python/samba/xattr.py b/python/samba/xattr.py new file mode 100644 index 00000000000..8516ba99cae --- /dev/null +++ b/python/samba/xattr.py @@ -0,0 +1,61 @@ +# Utility code for dealing with POSIX extended attributes +# +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2012 +# +# 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/>. + +from samba.dcerpc import xattr +import os +import samba.xattr_native +import shutil + + +def copyattrs(frompath, topath): + """Copy ACL related attributes from a path to another path.""" + for attr_name in (xattr.XATTR_NTACL_NAME, "system.posix_acl_access"): + # Get the xattr attributes if any + try: + attribute = samba.xattr_native.wrap_getxattr(frompath, + xattr.XATTR_NTACL_NAME) + samba.xattr_native.wrap_setxattr(topath, + xattr.XATTR_NTACL_NAME, + attribute) + except Exception: + pass + # FIXME:Catch a specific exception + + +def copytree_with_xattrs(src, dst): + """Recursively copy a directory tree using shutil.copy2(), preserving xattrs. + + The destination directory must not already exist. + If exception(s) occur, an Error is raised with a list of reasons. + """ + names = os.listdir(src) + + os.makedirs(dst) + errors = [] + for name in names: + srcname = os.path.join(src, name) + dstname = os.path.join(dst, name) + if os.path.islink(srcname): + linkto = os.readlink(srcname) + os.symlink(linkto, dstname) + elif os.path.isdir(srcname): + copytree_with_xattrs(srcname, dstname) + else: + # Will raise a SpecialFileError for unsupported file types + shutil.copy2(srcname, dstname) + shutil.copystat(src, dst) + copyattrs(src, dst) |