diff options
author | bubulle <bubulle@alioth.debian.org> | 2010-09-06 20:54:34 +0000 |
---|---|---|
committer | bubulle <bubulle@alioth.debian.org> | 2010-09-06 20:54:34 +0000 |
commit | 53601faba8f69c3454ad07acaceeef9165cb3743 (patch) | |
tree | b31a4174a7f4d2650717c1902a6bc3f922e13117 /source4/scripting/python | |
parent | 1b77db997b6a2ce389356509415a6e5e540bcfe0 (diff) | |
download | samba-53601faba8f69c3454ad07acaceeef9165cb3743.tar.gz |
Merge 3.5.4 in upstream branch
git-svn-id: svn://svn.debian.org/svn/pkg-samba/branches/samba/upstream@3574 fc4039ab-9d04-0410-8cac-899223bdd6b0
Diffstat (limited to 'source4/scripting/python')
25 files changed, 2288 insertions, 827 deletions
diff --git a/source4/scripting/python/STATUS b/source4/scripting/python/STATUS deleted file mode 100644 index ee67b8bc7a..0000000000 --- a/source4/scripting/python/STATUS +++ /dev/null @@ -1,14 +0,0 @@ -dsdb/samdb/ldb_modules/tests/samba3sam.py: Fix remaining failing tests -lib/ldb/tests/python/ldap.py: Fix remaining 3 FIXME's -command-line vampire -provisioning: combine some of the python dictionaries -finish scripting/bin/smbstatus.py - -not important before making Python the default: -- hierarchy (rename samr -> dcerpc.samr, misc -> samba.misc, etc) -- scripting/python/samba/upgrade.py -- install python modules into system -- SWAT -- __ndr_pack__/__ndr_unpack__ members for the NDR struct bindings -- generate docstrings in DCE/RPC bindings -- eliminate some variables from the python interface because they can be induced diff --git a/source4/scripting/python/config.mk b/source4/scripting/python/config.mk index ba624ee163..a5e3f25d59 100644 --- a/source4/scripting/python/config.mk +++ b/source4/scripting/python/config.mk @@ -17,7 +17,7 @@ python_uuid_OBJ_FILES = $(pyscriptsrcdir)/uuidmodule.o [PYTHON::python_glue] LIBRARY_REALNAME = samba/glue.$(SHLIBEXT) -PRIVATE_DEPENDENCIES = LIBNDR LIBLDB SAMDB CREDENTIALS pyldb python_dcerpc_misc python_dcerpc_security pyauth +PRIVATE_DEPENDENCIES = LIBNDR LIBLDB SAMDB CREDENTIALS pyldb python_dcerpc_misc python_dcerpc_security pyauth pyldb_util pyparam_util python_glue_OBJ_FILES = $(pyscriptsrcdir)/pyglue.o @@ -25,7 +25,7 @@ $(python_glue_OBJ_FILES): CFLAGS+=-I$(ldbsrcdir) _PY_FILES = $(shell find $(pyscriptsrcdir)/samba ../lib/subunit/python -name "*.py") -$(eval $(foreach pyfile, $(_PY_FILES),$(call python_py_module_template,$(patsubst $(pyscriptsrcdir)/%,%,$(pyfile)),$(pyfile)))) +$(eval $(foreach pyfile, $(_PY_FILES),$(call python_py_module_template,$(patsubst $(pyscriptsrcdir)/%,%,$(subst ../lib/subunit/python,,$(pyfile))),$(pyfile)))) EPYDOC_OPTIONS = --no-private --url http://www.samba.org/ --no-sourcecode diff --git a/source4/scripting/python/examples/samr.py b/source4/scripting/python/examples/samr.py index b3ea117b40..c0e3167a97 100755 --- a/source4/scripting/python/examples/samr.py +++ b/source4/scripting/python/examples/samr.py @@ -24,7 +24,7 @@ import sys sys.path.insert(0, "bin/python") -from samba.dcerpc import samr, security, lsa +from samba.dcerpc import samr, security def display_lsa_string(str): return str.string @@ -67,7 +67,7 @@ def test_EnumDomainUsers(samr, dom_handle): users = toArray(samr.EnumDomainUsers(dom_handle, 0, 0, -1)) print "Found %d users" % len(users) for idx, user in users: - print "\t%s\t(%d)" % (user, idx) + print "\t%s\t(%d)" % (user.string, idx) def test_EnumDomainGroups(samr, dom_handle): """test the samr_EnumDomainGroups interface""" @@ -75,7 +75,7 @@ def test_EnumDomainGroups(samr, dom_handle): groups = toArray(samr.EnumDomainGroups(dom_handle, 0, 0)) print "Found %d groups" % len(groups) for idx, group in groups: - print "\t%s\t(%d)" % (group, idx) + print "\t%s\t(%d)" % (group.string, idx) def test_domain_ops(samr, dom_handle): """test domain specific ops""" diff --git a/source4/scripting/python/pyglue.c b/source4/scripting/python/pyglue.c index a2c4790611..753f2df464 100644 --- a/source4/scripting/python/pyglue.c +++ b/source4/scripting/python/pyglue.c @@ -1,6 +1,7 @@ /* Unix SMB/CIFS implementation. Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007 + Copyright (C) Matthias Dieter Wallnöfer 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 @@ -19,6 +20,7 @@ #include "includes.h" #include "ldb.h" #include "ldb_errors.h" +#include "ldb_wrap.h" #include "param/param.h" #include "auth/credentials/credentials.h" #include "dsdb/samdb/samdb.h" @@ -31,6 +33,7 @@ #include "libcli/security/security.h" #include "auth/pyauth.h" #include "param/pyparam.h" +#include "auth/credentials/pycredentials.h" #ifndef Py_RETURN_NONE #define Py_RETURN_NONE return Py_INCREF(Py_None), Py_None @@ -38,15 +41,31 @@ /* FIXME: These should be in a header file somewhere, once we finish moving * away from SWIG .. */ -extern struct cli_credentials *cli_credentials_from_py_object(PyObject *py_obj); - #define PyErr_LDB_OR_RAISE(py_ldb, ldb) \ - if (!PyLdb_Check(py_ldb)) { \ - /*PyErr_SetString(PyExc_TypeError, "Ldb connection object required"); \ - return NULL; \ */ \ - } \ +/* if (!PyLdb_Check(py_ldb)) { \ + PyErr_SetString(py_ldb_get_exception(), "Ldb connection object required"); \ + return NULL; \ + } */\ ldb = PyLdb_AsLdbContext(py_ldb); +static void PyErr_SetLdbError(PyObject *error, int ret, struct ldb_context *ldb_ctx) +{ + if (ret == LDB_ERR_PYTHON_EXCEPTION) + return; /* Python exception should already be set, just keep that */ + + PyErr_SetObject(error, + Py_BuildValue(discard_const_p(char, "(i,s)"), ret, + ldb_ctx == NULL?ldb_strerror(ret):ldb_errstring(ldb_ctx))); +} + +static PyObject *py_ldb_get_exception(void) +{ + PyObject *mod = PyImport_ImportModule("ldb"); + if (mod == NULL) + return NULL; + + return PyObject_GetAttrString(mod, "LdbError"); +} static PyObject *py_generate_random_str(PyObject *self, PyObject *args) { @@ -74,6 +93,15 @@ static PyObject *py_unix2nttime(PyObject *self, PyObject *args) return PyInt_FromLong((uint64_t)nt); } +static PyObject *py_set_debug_level(PyObject *self, PyObject *args) +{ + unsigned level; + if (!PyArg_ParseTuple(args, "I", &level)) + return NULL; + (DEBUGLEVEL) = level; + Py_RETURN_NONE; +} + static PyObject *py_ldb_set_credentials(PyObject *self, PyObject *args) { PyObject *py_creds, *py_ldb; @@ -90,7 +118,7 @@ static PyObject *py_ldb_set_credentials(PyObject *self, PyObject *args) return NULL; } - ldb_set_opaque(ldb, "credentials", creds); + ldb_set_opaque(ldb, "credentials", creds); Py_RETURN_NONE; } @@ -138,6 +166,21 @@ static PyObject *py_ldb_set_session_info(PyObject *self, PyObject *args) Py_RETURN_NONE; } +static PyObject *py_ldb_set_utf8_casefold(PyObject *self, PyObject *args) +{ + PyObject *py_ldb; + struct ldb_context *ldb; + + if (!PyArg_ParseTuple(args, "O", &py_ldb)) + return NULL; + + PyErr_LDB_OR_RAISE(py_ldb, ldb); + + ldb_set_utf8_fns(ldb, NULL, wrap_casefold); + + Py_RETURN_NONE; +} + static PyObject *py_samdb_set_domain_sid(PyLdbObject *self, PyObject *args) { PyObject *py_ldb, *py_sid; @@ -160,6 +203,30 @@ static PyObject *py_samdb_set_domain_sid(PyLdbObject *self, PyObject *args) Py_RETURN_NONE; } +static PyObject *py_samdb_get_domain_sid(PyLdbObject *self, PyObject *args) +{ + PyObject *py_ldb; + struct ldb_context *ldb; + const struct dom_sid *sid; + PyObject *ret; + char *retstr; + + if (!PyArg_ParseTuple(args, "O", &py_ldb)) + return NULL; + + PyErr_LDB_OR_RAISE(py_ldb, ldb); + + sid = samdb_domain_sid(ldb); + if (!sid) { + PyErr_SetString(PyExc_RuntimeError, "samdb_domain_sid failed"); + return NULL; + } + retstr = dom_sid_string(NULL, sid); + ret = PyString_FromString(retstr); + talloc_free(retstr); + return ret; +} + static PyObject *py_ldb_register_samba_handlers(PyObject *self, PyObject *args) { PyObject *py_ldb; @@ -172,7 +239,7 @@ static PyObject *py_ldb_register_samba_handlers(PyObject *self, PyObject *args) PyErr_LDB_OR_RAISE(py_ldb, ldb); ret = ldb_register_samba_handlers(ldb); - PyErr_LDB_ERROR_IS_ERR_RAISE(ret, ldb); + PyErr_LDB_ERROR_IS_ERR_RAISE(py_ldb_get_exception(), ret, ldb); Py_RETURN_NONE; } @@ -196,6 +263,63 @@ static PyObject *py_dsdb_set_ntds_invocation_id(PyObject *self, PyObject *args) Py_RETURN_NONE; } +static PyObject *py_dsdb_set_opaque_integer(PyObject *self, PyObject *args) +{ + PyObject *py_ldb; + int value; + int *old_val, *new_val; + char *py_opaque_name, *opaque_name_talloc; + struct ldb_context *ldb; + TALLOC_CTX *tmp_ctx; + + if (!PyArg_ParseTuple(args, "Osi", &py_ldb, &py_opaque_name, &value)) + return NULL; + + PyErr_LDB_OR_RAISE(py_ldb, ldb); + + /* see if we have a cached copy */ + old_val = (int *)ldb_get_opaque(ldb, + py_opaque_name); + + if (old_val) { + *old_val = value; + Py_RETURN_NONE; + } + + tmp_ctx = talloc_new(ldb); + if (tmp_ctx == NULL) { + goto failed; + } + + new_val = talloc(tmp_ctx, int); + if (!new_val) { + goto failed; + } + + opaque_name_talloc = talloc_strdup(tmp_ctx, py_opaque_name); + if (!opaque_name_talloc) { + goto failed; + } + + *new_val = value; + + /* cache the domain_sid in the ldb */ + if (ldb_set_opaque(ldb, opaque_name_talloc, new_val) != LDB_SUCCESS) { + goto failed; + } + + talloc_steal(ldb, new_val); + talloc_steal(ldb, opaque_name_talloc); + talloc_free(tmp_ctx); + + Py_RETURN_NONE; + +failed: + talloc_free(tmp_ctx); + PyErr_SetString(PyExc_RuntimeError, "Failed to set opaque integer into the ldb!\n"); + return NULL; +} + static PyObject *py_dsdb_set_global_schema(PyObject *self, PyObject *args) { PyObject *py_ldb; @@ -207,12 +331,12 @@ static PyObject *py_dsdb_set_global_schema(PyObject *self, PyObject *args) PyErr_LDB_OR_RAISE(py_ldb, ldb); ret = dsdb_set_global_schema(ldb); - PyErr_LDB_ERROR_IS_ERR_RAISE(ret, ldb); + PyErr_LDB_ERROR_IS_ERR_RAISE(py_ldb_get_exception(), ret, ldb); Py_RETURN_NONE; } -static PyObject *py_dsdb_attach_schema_from_ldif_file(PyObject *self, PyObject *args) +static PyObject *py_dsdb_set_schema_from_ldif(PyObject *self, PyObject *args) { WERROR result; char *pf, *df; @@ -224,12 +348,107 @@ static PyObject *py_dsdb_attach_schema_from_ldif_file(PyObject *self, PyObject * PyErr_LDB_OR_RAISE(py_ldb, ldb); - result = dsdb_attach_schema_from_ldif_file(ldb, pf, df); + result = dsdb_set_schema_from_ldif(ldb, pf, df); PyErr_WERROR_IS_ERR_RAISE(result); Py_RETURN_NONE; } +static PyObject *py_dsdb_convert_schema_to_openldap(PyObject *self, PyObject *args) +{ + char *target_str, *mapping; + PyObject *py_ldb; + struct ldb_context *ldb; + PyObject *ret; + char *retstr; + + if (!PyArg_ParseTuple(args, "Oss", &py_ldb, &target_str, &mapping)) + return NULL; + + PyErr_LDB_OR_RAISE(py_ldb, ldb); + + retstr = dsdb_convert_schema_to_openldap(ldb, target_str, mapping); + if (!retstr) { + PyErr_SetString(PyExc_RuntimeError, "dsdb_convert_schema_to_openldap failed"); + return NULL; + } + ret = PyString_FromString(retstr); + talloc_free(retstr); + return ret; +} + +static PyObject *py_dsdb_write_prefixes_from_schema_to_ldb(PyObject *self, PyObject *args) +{ + PyObject *py_ldb; + struct ldb_context *ldb; + WERROR result; + struct dsdb_schema *schema; + + if (!PyArg_ParseTuple(args, "O", &py_ldb)) + return NULL; + + PyErr_LDB_OR_RAISE(py_ldb, ldb); + + schema = dsdb_get_schema(ldb); + if (!schema) { + PyErr_SetString(PyExc_RuntimeError, "Failed to set find a schema on ldb!\n"); + return NULL; + } + + result = dsdb_write_prefixes_from_schema_to_ldb(NULL, ldb, schema); + PyErr_WERROR_IS_ERR_RAISE(result); + + Py_RETURN_NONE; +} + +static PyObject *py_dsdb_set_schema_from_ldb(PyObject *self, PyObject *args) +{ + PyObject *py_ldb; + struct ldb_context *ldb; + PyObject *py_from_ldb; + struct ldb_context *from_ldb; + struct dsdb_schema *schema; + int ret; + if (!PyArg_ParseTuple(args, "OO", &py_ldb, &py_from_ldb)) + return NULL; + + PyErr_LDB_OR_RAISE(py_ldb, ldb); + + PyErr_LDB_OR_RAISE(py_from_ldb, from_ldb); + + schema = dsdb_get_schema(from_ldb); + if (!schema) { + PyErr_SetString(PyExc_RuntimeError, "Failed to set find a schema on 'from' ldb!\n"); + return NULL; + } + + ret = dsdb_reference_schema(ldb, schema, true); + PyErr_LDB_ERROR_IS_ERR_RAISE(py_ldb_get_exception(), ret, ldb); + + Py_RETURN_NONE; +} + +static PyObject *py_dom_sid_to_rid(PyLdbObject *self, PyObject *args) +{ + PyObject *py_sid; + struct dom_sid *sid; + uint32_t rid; + NTSTATUS status; + + if(!PyArg_ParseTuple(args, "O", &py_sid)) + return NULL; + + sid = dom_sid_parse_talloc(NULL, PyString_AsString(py_sid)); + + status = dom_sid_split_rid(NULL, sid, NULL, &rid); + if (!NT_STATUS_IS_OK(status)) { + PyErr_SetString(PyExc_RuntimeError, "dom_sid_split_rid failed"); + return NULL; + } + + return PyInt_FromLong(rid); +} + static PyMethodDef py_misc_methods[] = { { "generate_random_str", (PyCFunction)py_generate_random_str, METH_VARARGS, "random_password(len) -> string\n" @@ -248,15 +467,33 @@ static PyMethodDef py_misc_methods[] = { { "samdb_set_domain_sid", (PyCFunction)py_samdb_set_domain_sid, METH_VARARGS, "samdb_set_domain_sid(samdb, sid)\n" "Set SID of domain to use." }, + { "samdb_get_domain_sid", (PyCFunction)py_samdb_get_domain_sid, METH_VARARGS, + "samdb_get_domain_sid(samdb)\n" + "Get SID of domain in use." }, { "ldb_register_samba_handlers", (PyCFunction)py_ldb_register_samba_handlers, METH_VARARGS, "ldb_register_samba_handlers(ldb)\n" "Register Samba-specific LDB modules and schemas." }, + { "ldb_set_utf8_casefold", (PyCFunction)py_ldb_set_utf8_casefold, METH_VARARGS, + "ldb_set_utf8_casefold(ldb)\n" + "Set the right Samba casefolding function for UTF8 charset." }, { "dsdb_set_ntds_invocation_id", (PyCFunction)py_dsdb_set_ntds_invocation_id, METH_VARARGS, NULL }, + { "dsdb_set_opaque_integer", (PyCFunction)py_dsdb_set_opaque_integer, METH_VARARGS, + NULL }, { "dsdb_set_global_schema", (PyCFunction)py_dsdb_set_global_schema, METH_VARARGS, NULL }, - { "dsdb_attach_schema_from_ldif_file", (PyCFunction)py_dsdb_attach_schema_from_ldif_file, METH_VARARGS, + { "dsdb_set_schema_from_ldif", (PyCFunction)py_dsdb_set_schema_from_ldif, METH_VARARGS, + NULL }, + { "dsdb_write_prefixes_from_schema_to_ldb", (PyCFunction)py_dsdb_write_prefixes_from_schema_to_ldb, METH_VARARGS, NULL }, + { "dsdb_set_schema_from_ldb", (PyCFunction)py_dsdb_set_schema_from_ldb, METH_VARARGS, + NULL }, + { "dsdb_convert_schema_to_openldap", (PyCFunction)py_dsdb_convert_schema_to_openldap, METH_VARARGS, + NULL }, + { "dom_sid_to_rid", (PyCFunction)py_dom_sid_to_rid, METH_VARARGS, + NULL }, + { "set_debug_level", (PyCFunction)py_set_debug_level, METH_VARARGS, + "set debug level" }, { NULL } }; @@ -270,5 +507,47 @@ void initglue(void) return; PyModule_AddObject(m, "version", PyString_FromString(SAMBA_VERSION_STRING)); + + /* "userAccountControl" flags */ + PyModule_AddObject(m, "UF_NORMAL_ACCOUNT", PyInt_FromLong(UF_NORMAL_ACCOUNT)); + PyModule_AddObject(m, "UF_TEMP_DUPLICATE_ACCOUNT", PyInt_FromLong(UF_TEMP_DUPLICATE_ACCOUNT)); + PyModule_AddObject(m, "UF_SERVER_TRUST_ACCOUNT", PyInt_FromLong(UF_SERVER_TRUST_ACCOUNT)); + PyModule_AddObject(m, "UF_WORKSTATION_TRUST_ACCOUNT", PyInt_FromLong(UF_WORKSTATION_TRUST_ACCOUNT)); + PyModule_AddObject(m, "UF_INTERDOMAIN_TRUST_ACCOUNT", PyInt_FromLong(UF_INTERDOMAIN_TRUST_ACCOUNT)); + PyModule_AddObject(m, "UF_PASSWD_NOTREQD", PyInt_FromLong(UF_PASSWD_NOTREQD)); + PyModule_AddObject(m, "UF_ACCOUNTDISABLE", PyInt_FromLong(UF_ACCOUNTDISABLE)); + + /* "groupType" flags */ + PyModule_AddObject(m, "GTYPE_SECURITY_BUILTIN_LOCAL_GROUP", PyInt_FromLong(GTYPE_SECURITY_BUILTIN_LOCAL_GROUP)); + PyModule_AddObject(m, "GTYPE_SECURITY_GLOBAL_GROUP", PyInt_FromLong(GTYPE_SECURITY_GLOBAL_GROUP)); + PyModule_AddObject(m, "GTYPE_SECURITY_DOMAIN_LOCAL_GROUP", PyInt_FromLong(GTYPE_SECURITY_DOMAIN_LOCAL_GROUP)); + PyModule_AddObject(m, "GTYPE_SECURITY_UNIVERSAL_GROUP", PyInt_FromLong(GTYPE_SECURITY_UNIVERSAL_GROUP)); + PyModule_AddObject(m, "GTYPE_DISTRIBUTION_GLOBAL_GROUP", PyInt_FromLong(GTYPE_DISTRIBUTION_GLOBAL_GROUP)); + PyModule_AddObject(m, "GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP", PyInt_FromLong(GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP)); + PyModule_AddObject(m, "GTYPE_DISTRIBUTION_UNIVERSAL_GROUP", PyInt_FromLong(GTYPE_DISTRIBUTION_UNIVERSAL_GROUP)); + + /* "sAMAccountType" flags */ + PyModule_AddObject(m, "ATYPE_NORMAL_ACCOUNT", PyInt_FromLong(ATYPE_NORMAL_ACCOUNT)); + PyModule_AddObject(m, "ATYPE_WORKSTATION_TRUST", PyInt_FromLong(ATYPE_WORKSTATION_TRUST)); + PyModule_AddObject(m, "ATYPE_INTERDOMAIN_TRUST", PyInt_FromLong(ATYPE_INTERDOMAIN_TRUST)); + PyModule_AddObject(m, "ATYPE_SECURITY_GLOBAL_GROUP", PyInt_FromLong(ATYPE_SECURITY_GLOBAL_GROUP)); + PyModule_AddObject(m, "ATYPE_SECURITY_LOCAL_GROUP", PyInt_FromLong(ATYPE_SECURITY_LOCAL_GROUP)); + PyModule_AddObject(m, "ATYPE_SECURITY_UNIVERSAL_GROUP", PyInt_FromLong(ATYPE_SECURITY_UNIVERSAL_GROUP)); + PyModule_AddObject(m, "ATYPE_DISTRIBUTION_GLOBAL_GROUP", PyInt_FromLong(ATYPE_DISTRIBUTION_GLOBAL_GROUP)); + PyModule_AddObject(m, "ATYPE_DISTRIBUTION_LOCAL_GROUP", PyInt_FromLong(ATYPE_DISTRIBUTION_LOCAL_GROUP)); + PyModule_AddObject(m, "ATYPE_DISTRIBUTION_UNIVERSAL_GROUP", PyInt_FromLong(ATYPE_DISTRIBUTION_UNIVERSAL_GROUP)); + + /* "domainFunctionality", "forestFunctionality" flags in the rootDSE */ + PyModule_AddObject(m, "DS_DOMAIN_FUNCTION_2000", PyInt_FromLong(DS_DOMAIN_FUNCTION_2000)); + PyModule_AddObject(m, "DS_DOMAIN_FUNCTION_2003_MIXED", PyInt_FromLong(DS_DOMAIN_FUNCTION_2003_MIXED)); + PyModule_AddObject(m, "DS_DOMAIN_FUNCTION_2003", PyInt_FromLong(DS_DOMAIN_FUNCTION_2003)); + PyModule_AddObject(m, "DS_DOMAIN_FUNCTION_2008", PyInt_FromLong(DS_DOMAIN_FUNCTION_2008)); + PyModule_AddObject(m, "DS_DOMAIN_FUNCTION_2008_R2", PyInt_FromLong(DS_DOMAIN_FUNCTION_2008_R2)); + + /* "domainControllerFunctionality" flags in the rootDSE */ + PyModule_AddObject(m, "DS_DC_FUNCTION_2000", PyInt_FromLong(DS_DC_FUNCTION_2000)); + PyModule_AddObject(m, "DS_DC_FUNCTION_2003", PyInt_FromLong(DS_DC_FUNCTION_2003)); + PyModule_AddObject(m, "DS_DC_FUNCTION_2008", PyInt_FromLong(DS_DC_FUNCTION_2008)); + PyModule_AddObject(m, "DS_DC_FUNCTION_2008_R2", PyInt_FromLong(DS_DC_FUNCTION_2008_R2)); } diff --git a/source4/scripting/python/samba/__init__.py b/source4/scripting/python/samba/__init__.py index a49e6e1ead..82df4960cf 100644 --- a/source4/scripting/python/samba/__init__.py +++ b/source4/scripting/python/samba/__init__.py @@ -28,7 +28,7 @@ import os def _in_source_tree(): """Check whether the script is being run from the source dir. """ - return os.path.exists("%s/../../../samba4-skip" % os.path.dirname(__file__)) + return os.path.exists("%s/../../../selftest/skip" % os.path.dirname(__file__)) # When running, in-tree, make sure bin/python is in the PYTHONPATH @@ -42,7 +42,6 @@ else: import ldb -import credentials import glue class Ldb(ldb.Ldb): @@ -53,44 +52,59 @@ class Ldb(ldb.Ldb): not necessarily the Sam database. For Sam-specific helper functions see samdb.py. """ - def __init__(self, url=None, session_info=None, credentials=None, - modules_dir=None, lp=None): - """Open a Samba Ldb file. + 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 modules_dir: Modules directory, if not the default. - :param lp: Loadparm object, optional. + :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). """ - super(Ldb, self).__init__() if modules_dir is not None: self.set_modules_dir(modules_dir) elif default_ldb_modules_dir is not None: self.set_modules_dir(default_ldb_modules_dir) - - if credentials is not None: - self.set_credentials(credentials) + elif lp is not None: + self.set_modules_dir(os.path.join(lp.get("modules dir"), "ldb")) if session_info is not None: self.set_session_info(session_info) - glue.ldb_register_samba_handlers(self) + 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 + glue.ldb_register_samba_handlers(self) + + # TODO set debug def msg(l,text): print text #self.set_debug(msg) + glue.ldb_set_utf8_casefold(self) + + # 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 == true: + flags |= FLG_NOSYNC + if url is not None: - self.connect(url) + self.connect(url, flags, options) def set_credentials(self, credentials): glue.ldb_set_credentials(self, credentials) @@ -118,62 +132,100 @@ class Ldb(ldb.Ldb): 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, (ldb.ERR_NO_SUCH_OBJECT, _): + # Ignore no such object errors + return + pass + + try: + for msg in res: + self.delete(msg.dn) + except ldb.LdbError, (ldb.ERR_NO_SUCH_OBJECT, _): + # Ignore no such object errors + return + + def erase_except_schema_controlled(self): + """Erase this ldb, removing 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"]): + try: + self.delete(msg.dn) + except ldb.LdbError, (ldb.ERR_NO_SUCH_OBJECT, _): + # Ignore no such object errors + pass + + res = self.search(basedn, ldb.SCOPE_SUBTREE, + "(&(|(objectclass=*)(distinguishedName=*))(!(distinguishedName=@BASEINFO)))", + [], controls=["show_deleted:0"]) + assert len(res) == 0 + + # delete the specials + for attr in ["@SUBCLASSES", "@MODULES", + "@OPTIONS", "@PARTITION", "@KLUDGEACL"]: + try: + self.delete(attr) + except ldb.LdbError, (ldb.ERR_NO_SUCH_OBJECT, _): + # Ignore missing dn errors + pass + def erase(self): """Erase this ldb, removing all records.""" + + self.erase_except_schema_controlled() + # delete the specials - for attr in ["@INDEXLIST", "@ATTRIBUTES", "@SUBCLASSES", "@MODULES", - "@OPTIONS", "@PARTITION", "@KLUDGEACL"]: + for attr in ["@INDEXLIST", "@ATTRIBUTES"]: try: self.delete(attr) - except ldb.LdbError, (LDB_ERR_NO_SUCH_OBJECT, _): + except ldb.LdbError, (ldb.ERR_NO_SUCH_OBJECT, _): # Ignore missing dn errors pass - basedn = "" - # and the rest - for msg in self.search(basedn, ldb.SCOPE_SUBTREE, - "(&(|(objectclass=*)(distinguishedName=*))(!(distinguishedName=@BASEINFO)))", - ["distinguishedName"]): + def erase_partitions(self): + """Erase an ldb, removing all records.""" + + def erase_recursive(self, dn): try: - self.delete(msg.dn) - except ldb.LdbError, (LDB_ERR_NO_SUCH_OBJECT, _): + res = self.search(base=dn, scope=ldb.SCOPE_ONELEVEL, attrs=[], + controls=["show_deleted:0"]) + except ldb.LdbError, (ldb.ERR_NO_SUCH_OBJECT, _): # Ignore no such object errors + return pass + + for msg in res: + erase_recursive(self, msg.dn) - res = self.search(basedn, ldb.SCOPE_SUBTREE, "(&(|(objectclass=*)(distinguishedName=*))(!(distinguishedName=@BASEINFO)))", ["distinguishedName"]) - assert len(res) == 0 + try: + self.delete(dn) + except ldb.LdbError, (ldb.ERR_NO_SUCH_OBJECT, _): + # Ignore no such object errors + pass - def erase_partitions(self): - """Erase an ldb, removing all records.""" res = self.search("", ldb.SCOPE_BASE, "(objectClass=*)", ["namingContexts"]) assert len(res) == 1 if not "namingContexts" in res[0]: return for basedn in res[0]["namingContexts"]: - previous_remaining = 1 - current_remaining = 0 - - k = 0 - while ++k < 10 and (previous_remaining != current_remaining): - # and the rest - try: - res2 = self.search(basedn, ldb.SCOPE_SUBTREE, "(|(objectclass=*)(distinguishedName=*))", ["distinguishedName"]) - except ldb.LdbError, (LDB_ERR_NO_SUCH_OBJECT, _): - # Ignore missing dn errors - return - - previous_remaining = current_remaining - current_remaining = len(res2) - for msg in res2: - try: - self.delete(msg.dn) - # Ignore no such object errors - except ldb.LdbError, (LDB_ERR_NO_SUCH_OBJECT, _): - pass - # Ignore not allowed on non leaf errors - except ldb.LdbError, (LDB_ERR_NOT_ALLOWED_ON_NON_LEAF, _): - pass + # Try to delete user/computer accounts to allow deletion of groups + self.erase_users_computers(basedn) + # Try and erase from the bottom-up in the tree + erase_recursive(self, basedn) def load_ldif_file_add(self, ldif_path): """Load a LDIF file. @@ -199,6 +251,46 @@ class Ldb(ldb.Ldb): for changetype, msg in self.parse_ldif(ldif): self.modify(msg) + def set_domain_sid(self, sid): + """Change the domain SID used by this LDB. + + :param sid: The new domain sid to use. + """ + glue.samdb_set_domain_sid(self, sid) + + def domain_sid(self): + """Read the domain SID used by this LDB. + + """ + glue.samdb_get_domain_sid(self) + + def set_schema_from_ldif(self, pf, df): + glue.dsdb_set_schema_from_ldif(self, pf, df) + + def set_schema_from_ldb(self, ldb): + glue.dsdb_set_schema_from_ldb(self, ldb) + + def write_prefixes_from_schema(self): + glue.dsdb_write_prefixes_from_schema_to_ldb(self) + + def convert_schema_to_openldap(self, target, mapping): + return glue.dsdb_convert_schema_to_openldap(self, target, mapping) + + def set_invocation_id(self, invocation_id): + """Set the invocation id for this SamDB handle. + + :param invocation_id: GUID of the invocation id. + """ + glue.dsdb_set_ntds_invocation_id(self, invocation_id) + + def set_opaque_integer(self, name, value): + """Set an integer as an opaque (a flag or other value) value on the database + + :param name: The name for the opaque value + :param value: The integer value + """ + glue.dsdb_set_opaque_integer(self, name, value) + def substitute_var(text, values): """substitute strings of the form ${NAME} in str, replacing @@ -233,10 +325,65 @@ def check_all_substituted(text): def valid_netbios_name(name): """Check whether a name is valid as a NetBIOS name. """ - # FIXME: There are probably more constraints here. - # crh has a paragraph on this in his book (1.4.1.1) + # See crh's book (1.4.1.1) if len(name) > 15: return False + for x in name: + if not x.isalnum() and not x in " !#$%&'()-.@^_{}~": + return False return True + +def dom_sid_to_rid(sid_str): + """Converts a domain SID to the relative RID. + + :param sid_str: The domain SID formatted as string + """ + + return glue.dom_sid_to_rid(sid_str) + + version = glue.version + +# "userAccountControl" flags +UF_NORMAL_ACCOUNT = glue.UF_NORMAL_ACCOUNT +UF_TEMP_DUPLICATE_ACCOUNT = glue.UF_TEMP_DUPLICATE_ACCOUNT +UF_SERVER_TRUST_ACCOUNT = glue.UF_SERVER_TRUST_ACCOUNT +UF_WORKSTATION_TRUST_ACCOUNT = glue.UF_WORKSTATION_TRUST_ACCOUNT +UF_INTERDOMAIN_TRUST_ACCOUNT = glue.UF_INTERDOMAIN_TRUST_ACCOUNT +UF_PASSWD_NOTREQD = glue.UF_PASSWD_NOTREQD +UF_ACCOUNTDISABLE = glue.UF_ACCOUNTDISABLE + +# "groupType" flags +GTYPE_SECURITY_BUILTIN_LOCAL_GROUP = glue.GTYPE_SECURITY_BUILTIN_LOCAL_GROUP +GTYPE_SECURITY_GLOBAL_GROUP = glue.GTYPE_SECURITY_GLOBAL_GROUP +GTYPE_SECURITY_DOMAIN_LOCAL_GROUP = glue.GTYPE_SECURITY_DOMAIN_LOCAL_GROUP +GTYPE_SECURITY_UNIVERSAL_GROUP = glue.GTYPE_SECURITY_UNIVERSAL_GROUP +GTYPE_DISTRIBUTION_GLOBAL_GROUP = glue.GTYPE_DISTRIBUTION_GLOBAL_GROUP +GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP = glue.GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP +GTYPE_DISTRIBUTION_UNIVERSAL_GROUP = glue.GTYPE_DISTRIBUTION_UNIVERSAL_GROUP + +# "sAMAccountType" flags +ATYPE_NORMAL_ACCOUNT = glue.ATYPE_NORMAL_ACCOUNT +ATYPE_WORKSTATION_TRUST = glue.ATYPE_WORKSTATION_TRUST +ATYPE_INTERDOMAIN_TRUST = glue.ATYPE_INTERDOMAIN_TRUST +ATYPE_SECURITY_GLOBAL_GROUP = glue.ATYPE_SECURITY_GLOBAL_GROUP +ATYPE_SECURITY_LOCAL_GROUP = glue.ATYPE_SECURITY_LOCAL_GROUP +ATYPE_SECURITY_UNIVERSAL_GROUP = glue.ATYPE_SECURITY_UNIVERSAL_GROUP +ATYPE_DISTRIBUTION_GLOBAL_GROUP = glue.ATYPE_DISTRIBUTION_GLOBAL_GROUP +ATYPE_DISTRIBUTION_LOCAL_GROUP = glue.ATYPE_DISTRIBUTION_LOCAL_GROUP +ATYPE_DISTRIBUTION_UNIVERSAL_GROUP = glue.ATYPE_DISTRIBUTION_UNIVERSAL_GROUP + +# "domainFunctionality", "forestFunctionality" flags in the rootDSE */ +DS_DOMAIN_FUNCTION_2000 = glue.DS_DOMAIN_FUNCTION_2000 +DS_DOMAIN_FUNCTION_2003_MIXED = glue.DS_DOMAIN_FUNCTION_2003_MIXED +DS_DOMAIN_FUNCTION_2003 = glue.DS_DOMAIN_FUNCTION_2003 +DS_DOMAIN_FUNCTION_2008 = glue.DS_DOMAIN_FUNCTION_2008 +DS_DOMAIN_FUNCTION_2008_R2 = glue.DS_DOMAIN_FUNCTION_2008_R2 + +# "domainControllerFunctionality" flags in the rootDSE */ +DS_DC_FUNCTION_2000 = glue.DS_DC_FUNCTION_2000 +DS_DC_FUNCTION_2003 = glue.DS_DC_FUNCTION_2003 +DS_DC_FUNCTION_2008 = glue.DS_DC_FUNCTION_2008 +DS_DC_FUNCTION_2008_R2 = glue.DS_DC_FUNCTION_2008_R2 + diff --git a/source4/scripting/python/samba/getopt.py b/source4/scripting/python/samba/getopt.py index c12245f6c5..8b756b2d6f 100644 --- a/source4/scripting/python/samba/getopt.py +++ b/source4/scripting/python/samba/getopt.py @@ -20,7 +20,7 @@ """Support for parsing Samba-related command-line options.""" import optparse -from credentials import Credentials, AUTO_USE_KERBEROS, DONT_USE_KERBEROS, MUST_USE_KERBEROS +from credentials import Credentials, DONT_USE_KERBEROS, MUST_USE_KERBEROS from hostconfig import Hostconfig __docformat__ = "restructuredText" diff --git a/source4/scripting/python/samba/idmap.py b/source4/scripting/python/samba/idmap.py index f8eeb18925..ad209f42de 100644 --- a/source4/scripting/python/samba/idmap.py +++ b/source4/scripting/python/samba/idmap.py @@ -23,8 +23,6 @@ __docformat__ = "restructuredText" import samba -import glue -import ldb class IDmapDB(samba.Ldb): """The IDmap database.""" @@ -34,23 +32,23 @@ class IDmapDB(samba.Ldb): TYPE_GID = 2 TYPE_BOTH = 3 - def __init__(self, url=None, session_info=None, credentials=None, - modules_dir=None, lp=None): - """Open the IDmap Database. - - :param url: URL of the database. + 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.get("idmap database") - super(IDmapDB, self).__init__(session_info=session_info, credentials=credentials, - modules_dir=modules_dir, lp=lp) - if url: - self.connect(url) - else: - self.connect(lp.get("idmap database")) + 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): - super(IDmapDB, self).connect(self.lp.private_path(url)) + def connect(self, url=None, flags=0, options=None): + super(IDmapDB, self).connect(url=self.lp.private_path(url), flags=flags, + options=options) def setup_name_mapping(self, sid, type, unixid): """Setup a mapping between a sam name and a unix name. diff --git a/source4/scripting/python/samba/ms_display_specifiers.py b/source4/scripting/python/samba/ms_display_specifiers.py new file mode 100644 index 0000000000..2a54e4ae0e --- /dev/null +++ b/source4/scripting/python/samba/ms_display_specifiers.py @@ -0,0 +1,189 @@ +#!/usr/bin/python +# +# 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/source4/scripting/python/samba/ms_schema.py b/source4/scripting/python/samba/ms_schema.py new file mode 100644 index 0000000000..a0abc337ce --- /dev/null +++ b/source4/scripting/python/samba/ms_schema.py @@ -0,0 +1,276 @@ +#!/usr/bin/env python +# +# create schema.ldif (as a string) from WSPP documentation +# +# based on minschema.py and minschema_wspp +# + +import re +import base64 + +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]) + + 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/source4/scripting/python/samba/provision.py b/source4/scripting/python/samba/provision.py index d96857661e..64491c2b18 100644 --- a/source4/scripting/python/samba/provision.py +++ b/source4/scripting/python/samba/provision.py @@ -3,7 +3,7 @@ # backend code for provisioning a Samba4 server # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2008 -# Copyright (C) Andrew Bartlett <abartlet@samba.org> 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: @@ -36,18 +36,28 @@ import socket import param import registry import samba -from auth import system_session -from samba import Ldb, substitute_var, valid_netbios_name, check_all_substituted +import subprocess +import ldb + +import shutil +from credentials import Credentials, DONT_USE_KERBEROS +from auth import system_session, admin_session +from samba import version, Ldb, substitute_var, valid_netbios_name +from samba import check_all_substituted +from samba import DS_DOMAIN_FUNCTION_2000, DS_DOMAIN_FUNCTION_2008, DS_DC_FUNCTION_2008, DS_DC_FUNCTION_2008_R2 from samba.samdb import SamDB from samba.idmap import IDmapDB from samba.dcerpc import security +from samba.ndr import ndr_pack import urllib -from ldb import SCOPE_SUBTREE, SCOPE_ONELEVEL, SCOPE_BASE, LdbError, \ - timestring, CHANGETYPE_MODIFY, CHANGETYPE_NONE +from ldb import SCOPE_SUBTREE, SCOPE_ONELEVEL, SCOPE_BASE, LdbError, timestring +from ms_schema import read_ms_schema +from ms_display_specifiers import read_ms_ldif +from signal import SIGTERM +from dcerpc.misc import SEC_CHAN_BDC, SEC_CHAN_WKSTA __docformat__ = "restructuredText" - def find_setup_dir(): """Find the setup directory used by provision.""" dirname = os.path.dirname(__file__) @@ -63,9 +73,47 @@ def find_setup_dir(): return ret raise Exception("Unable to find setup directory.") +def get_schema_descriptor(domain_sid): + sddl = "O:SAG:SAD:(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)" \ + "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 b64encode(ndr_pack(sec)) + +def get_config_descriptor(domain_sid): + 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;;S-1-5-21-3191434175-1265308384-3577286990-498)" \ + "S:(AU;SA;WPWOWD;;;WD)(AU;SA;CR;;;BA)(AU;SA;CR;;;DU)" \ + "(OU;SA;CR;45ec5156-db7e-47bb-b53f-dbeb2d03c40f;;WD)" + sec = security.descriptor.from_sddl(sddl, domain_sid) + return b64encode(ndr_pack(sec)) + DEFAULTSITE = "Default-First-Site-Name" +# Exception classes + +class ProvisioningError(Exception): + """A generic provision error.""" + class InvalidNetbiosName(Exception): """A specified name was not a valid NetBIOS name.""" def __init__(self, name): @@ -95,11 +143,12 @@ class ProvisionPaths(object): self.memberofconf = None self.fedoradsinf = None self.fedoradspartitions = None + self.fedoradssasl = None self.olmmron = None self.olmmrserveridsconf = None self.olmmrsyncreplconf = None self.olcdir = None - self.olslaptest = None + self.olslapd = None self.olcseedldif = None @@ -109,6 +158,7 @@ class ProvisionNames(object): self.domaindn = None self.configdn = None self.schemadn = None + self.sambadn = None self.ldapmanagerdn = None self.dnsdomain = None self.realm = None @@ -125,7 +175,72 @@ class ProvisionResult(object): self.domaindn = None self.lp = None self.samdb = None + +class Schema(object): + def __init__(self, setup_path, domain_sid, schemadn=None, + serverdn=None, sambadn=None, ldap_backend_type=None): + """Load schema for the SamDB from the AD schema files and samba4_schema.ldif + + :param samdb: Load a schema into a SamDB. + :param setup_path: Setup path function. + :param schemadn: DN of the schema + :param serverdn: DN of the server + + Returns the schema data loaded, to avoid double-parsing when then needing to add it to the db + """ + + self.ldb = Ldb() + self.schema_data = read_ms_schema(setup_path('ad-schema/MS-AD_Schema_2K8_Attributes.txt'), + setup_path('ad-schema/MS-AD_Schema_2K8_Classes.txt')) + self.schema_data += open(setup_path("schema_samba4.ldif"), '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, + "SERVERDN": serverdn, + }) + + descr = get_schema_descriptor(domain_sid) + self.schema_dn_add = read_and_sub_file(setup_path("provision_schema_basedn.ldif"), + {"SCHEMADN": schemadn, + "DESCRIPTOR": descr + }) + prefixmap = open(setup_path("prefixMap.txt"), 'r').read() + prefixmap = b64encode(prefixmap) + + # We don't actually add this ldif, just parse it + prefixmap_ldif = "dn: cn=schema\nprefixMap:: %s\n\n" % prefixmap + self.ldb.set_schema_from_ldif(prefixmap_ldif, self.schema_data) + + +# 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): + attrs = ["linkID", "lDAPDisplayName"] + res = schemaldb.search(expression="(&(!(linkID=*))(objectclass=attributeSchema)(attributeSyntax=2.5.5.1))", base=schemadn, scope=SCOPE_ONELEVEL, attrs=attrs) + attributes = [] + for i in range (0, len(res)): + attributes.append(str(res[i]["lDAPDisplayName"])) + + return attributes + + def check_install(lp, session_info, credentials): """Check whether the current install seems ok. @@ -138,7 +253,7 @@ def check_install(lp, session_info, credentials): ldb = Ldb(lp.get("sam database"), session_info=session_info, credentials=credentials, lp=lp) if len(ldb.search("(cn=Administrator)")) != 1: - raise "No administrator account found" + raise ProvisioningError("No administrator account found") def findnss(nssfn, names): @@ -241,14 +356,12 @@ def provision_paths_from_lp(lp, dnsdomain): """ paths = ProvisionPaths() paths.private_dir = lp.get("private dir") - paths.keytab = "secrets.keytab" paths.dns_keytab = "dns.keytab" paths.shareconf = os.path.join(paths.private_dir, "share.ldb") paths.samdb = os.path.join(paths.private_dir, lp.get("sam database") or "samdb.ldb") paths.idmapdb = os.path.join(paths.private_dir, lp.get("idmap database") or "idmap.ldb") paths.secrets = os.path.join(paths.private_dir, lp.get("secrets database") or "secrets.ldb") - paths.templates = os.path.join(paths.private_dir, "templates.ldb") paths.dns = os.path.join(paths.private_dir, dnsdomain + ".zone") paths.namedconf = os.path.join(paths.private_dir, "named.conf") paths.namedtxt = os.path.join(paths.private_dir, "named.txt") @@ -261,6 +374,8 @@ def provision_paths_from_lp(lp, dnsdomain): "ldap") paths.slapdconf = os.path.join(paths.ldapdir, "slapd.conf") + paths.slapdpid = os.path.join(paths.ldapdir, + "slapd.pid") paths.modulesconf = os.path.join(paths.ldapdir, "modules.conf") paths.memberofconf = os.path.join(paths.ldapdir, @@ -269,6 +384,10 @@ def provision_paths_from_lp(lp, dnsdomain): "fedorads.inf") paths.fedoradspartitions = os.path.join(paths.ldapdir, "fedorads-partitions.ldif") + paths.fedoradssasl = os.path.join(paths.ldapdir, + "fedorads-sasl.ldif") + paths.fedoradssamba = os.path.join(paths.ldapdir, + "fedorads-samba.ldif") paths.olmmrserveridsconf = os.path.join(paths.ldapdir, "mmr_serverids.conf") paths.olmmrsyncreplconf = os.path.join(paths.ldapdir, @@ -293,9 +412,9 @@ def provision_paths_from_lp(lp, dnsdomain): return paths -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): +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, sambadn=None): """Guess configuration settings to use.""" if hostname is None: @@ -340,6 +459,15 @@ def guess_names(lp=None, hostname=None, domain=None, dnsdomain=None, serverrole= if not valid_netbios_name(domain): raise InvalidNetbiosName(domain) + if netbiosname.upper() == realm.upper(): + raise Exception("realm %s must not be equal to netbios domain name %s", realm, netbiosname) + + if hostname.upper() == realm.upper(): + raise Exception("realm %s must not be equal to hostname %s", realm, hostname) + + if domain.upper() == realm.upper(): + raise Exception("realm %s must not be equal to domain name %s", realm, domain) + if rootdn is None: rootdn = domaindn @@ -347,6 +475,8 @@ def guess_names(lp=None, hostname=None, domain=None, dnsdomain=None, serverrole= configdn = "CN=Configuration," + rootdn if schemadn is None: schemadn = "CN=Schema," + configdn + if sambadn is None: + sambadn = "CN=Samba" if sitename is None: sitename=DEFAULTSITE @@ -356,6 +486,7 @@ def guess_names(lp=None, hostname=None, domain=None, dnsdomain=None, serverrole= names.domaindn = domaindn names.configdn = configdn names.schemadn = schemadn + names.sambadn = sambadn names.ldapmanagerdn = "CN=Manager," + rootdn names.dnsdomain = dnsdomain names.domain = domain @@ -432,20 +563,17 @@ def setup_name_mappings(samdb, idmap, sid, domaindn, root_uid, nobody_uid, :param nobody_uid: uid of the UNIX nobody user. :param users_gid: gid of the UNIX users group. :param wheel_gid: gid of the UNIX wheel group.""" - # add some foreign sids if they are not present already - samdb.add_stock_foreign_sids() idmap.setup_name_mapping("S-1-5-7", idmap.TYPE_UID, nobody_uid) idmap.setup_name_mapping("S-1-5-32-544", idmap.TYPE_GID, wheel_gid) - + 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, setup_path, message, lp, session_info, credentials, names, serverrole, ldap_backend=None, - ldap_backend_type=None, erase=False): + erase=False): """Setup the partitions for the SAM database. Alternatively, provision() may call this, and then populate the database. @@ -458,17 +586,20 @@ def setup_samdb_partitions(samdb_path, setup_path, message, lp, session_info, """ 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: - samdb = SamDB(samdb_path, session_info=session_info, - credentials=credentials, lp=lp) + samdb = Ldb(url=samdb_path, session_info=session_info, + credentials=credentials, lp=lp, options=["modules:"]) # Wipes the database - samdb.erase() + samdb.erase_except_schema_controlled() except LdbError: os.unlink(samdb_path) - samdb = SamDB(samdb_path, session_info=session_info, - credentials=credentials, lp=lp) + samdb = Ldb(url=samdb_path, session_info=session_info, + credentials=credentials, lp=lp, options=["modules:"]) # Wipes the database - samdb.erase() + samdb.erase_except_schema_controlled() #Add modules to the list to activate them by default @@ -481,7 +612,9 @@ def setup_samdb_partitions(samdb_path, setup_path, message, lp, session_info, # module when expanding the objectclass list) # - partition must be last # - each partition has its own module list then - modules_list = ["rootdse", + modules_list = ["resolve_oids", + "rootdse", + "acl", "paged_results", "ranged_results", "anr", @@ -491,10 +624,11 @@ def setup_samdb_partitions(samdb_path, setup_path, message, lp, session_info, "extended_dn_in", "rdn_name", "objectclass", + "descriptor", "samldb", - "kludge_acl", "password_hash", - "operational"] + "operational", + "kludge_acl"] tdb_modules_list = [ "subtree_rename", "subtree_delete", @@ -504,28 +638,25 @@ def setup_samdb_partitions(samdb_path, setup_path, message, lp, session_info, "partition"] domaindn_ldb = "users.ldb" - if ldap_backend is not None: - domaindn_ldb = ldap_backend configdn_ldb = "configuration.ldb" - if ldap_backend is not None: - configdn_ldb = ldap_backend schemadn_ldb = "schema.ldb" if ldap_backend is not None: - schema_ldb = ldap_backend - schemadn_ldb = ldap_backend + domaindn_ldb = ldap_backend.ldapi_uri + configdn_ldb = ldap_backend.ldapi_uri + schemadn_ldb = ldap_backend.ldapi_uri - if ldap_backend_type == "fedora-ds": - backend_modules = ["nsuniqueid", "paged_searches"] - # We can handle linked attributes here, as we don't have directory-side subtree operations - tdb_modules_list = ["linked_attributes", "extended_dn_out_dereference"] - elif ldap_backend_type == "openldap": - backend_modules = ["entryuuid", "paged_searches"] - # OpenLDAP handles subtree renames, so we don't want to do any of these things - tdb_modules_list = ["extended_dn_out_dereference"] - elif ldap_backend is not None: - raise "LDAP Backend specified, but LDAP Backend Type not specified" + if ldap_backend.ldap_backend_type == "fedora-ds": + backend_modules = ["nsuniqueid", "paged_searches"] + # We can handle linked attributes here, as we don't have directory-side subtree operations + tdb_modules_list = ["linked_attributes", "extended_dn_out_dereference"] + elif ldap_backend.ldap_backend_type == "openldap": + backend_modules = ["entryuuid", "paged_searches"] + # OpenLDAP handles subtree renames, so we don't want to do any of these things + tdb_modules_list = ["extended_dn_out_dereference"] + elif serverrole == "domain controller": - backend_modules = ["repl_meta_data"] + tdb_modules_list.insert(0, "repl_meta_data") + backend_modules = [] else: backend_modules = ["objectguid"] @@ -536,6 +667,7 @@ def setup_samdb_partitions(samdb_path, setup_path, message, lp, session_info, samdb.transaction_start() try: + message("Setting up sam.ldb partitions and settings") setup_add_ldif(samdb, setup_path("provision_partitions.ldif"), { "SCHEMADN": names.schemadn, "SCHEMADN_LDB": schemadn_ldb, @@ -553,54 +685,94 @@ def setup_samdb_partitions(samdb_path, setup_path, message, lp, session_info, "BACKEND_MOD": ",".join(backend_modules), }) - except: - samdb.transaction_cancel() - raise - - samdb.transaction_commit() - - samdb = SamDB(samdb_path, session_info=session_info, - credentials=credentials, lp=lp) - - samdb.transaction_start() - try: - message("Setting up sam.ldb attributes") samdb.load_ldif_file_add(setup_path("provision_init.ldif")) message("Setting up sam.ldb rootDSE") setup_samdb_rootdse(samdb, setup_path, names) - if erase: - message("Erasing data from partitions") - samdb.erase_partitions() - except: samdb.transaction_cancel() raise samdb.transaction_commit() - return samdb +def secretsdb_self_join(secretsdb, domain, + netbiosname, domainsid, machinepass, + 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"] + + + msg = ldb.Message(ldb.Dn(secretsdb, "flatname=%s,cn=Primary Domains" % domain)); + msg["secureChannelType"] = str(secure_channel_type) + msg["flatname"] = [domain] + msg["objectClass"] = ["top", "primaryDomain"] + if realm is not None: + if dnsdomain is None: + dnsdomain = realm.lower() + msg["objectClass"] = ["top", "primaryDomain", "kerberosSecret"] + msg["realm"] = realm + msg["saltPrincipal"] = "host/%s.%s@%s" % (netbiosname.lower(), dnsdomain.lower(), 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)] + msg["objectSid"] = [ndr_pack(domainsid)] + + res = secretsdb.search(base="cn=Primary Domains", + attrs=attrs, + expression=("(&(|(flatname=%s)(realm=%s)(objectSid=%s))(objectclass=primaryDomain))" % (domain, realm, str(domainsid))), + scope=SCOPE_ONELEVEL) + + for del_msg in res: + if del_msg.dn is not msg.dn: + secretsdb.delete(del_msg.dn) + + res = secretsdb.search(base=msg.dn, attrs=attrs, scope=SCOPE_BASE) + + if len(res) == 1: + msg["priorSecret"] = res[0]["secret"] + msg["priorWhenChanged"] = res[0]["whenChanged"] + + if res["privateKeytab"] is not None: + msg["privateKeytab"] = res[0]["privateKeytab"] + if res["krb5Keytab"] is not None: + msg["krb5Keytab"] = res[0]["krb5Keytab"] -def secretsdb_become_dc(secretsdb, setup_path, domain, realm, dnsdomain, - netbiosname, domainsid, keytab_path, samdb_url, - dns_keytab_path, dnspass, machinepass): - """Add DC-specific bits to a secrets database. + for el in msg: + el.set_flags(ldb.FLAG_MOD_REPLACE) + secretsdb.modify(msg) + else: + secretsdb.add(msg) + + +def secretsdb_setup_dns(secretsdb, setup_path, realm, dnsdomain, + dns_keytab_path, dnspass): + """Add DNS specific bits to a secrets database. :param secretsdb: Ldb Handle to the secrets database :param setup_path: Setup path function :param machinepass: Machine password """ - setup_ldb(secretsdb, setup_path("secrets_dc.ldif"), { - "MACHINEPASS_B64": b64encode(machinepass), - "DOMAIN": domain, + setup_ldb(secretsdb, setup_path("secrets_dns.ldif"), { "REALM": realm, "DNSDOMAIN": dnsdomain, - "DOMAINSID": str(domainsid), - "SECRETS_KEYTAB": keytab_path, - "NETBIOSNAME": netbiosname, - "SAM_LDB": samdb_url, "DNS_KEYTAB": dns_keytab_path, "DNSPASS_B64": b64encode(dnspass), }) @@ -624,6 +796,7 @@ def setup_secretsdb(path, setup_path, session_info, credentials, lp): secrets_ldb.load_ldif_file_add(setup_path("secrets_init.ldif")) secrets_ldb = Ldb(path, session_info=session_info, credentials=credentials, lp=lp) + secrets_ldb.transaction_start() secrets_ldb.load_ldif_file_add(setup_path("secrets.ldif")) if credentials is not None and credentials.authentication_requested(): @@ -641,34 +814,7 @@ def setup_secretsdb(path, setup_path, session_info, credentials, lp): return secrets_ldb - -def setup_templatesdb(path, setup_path, session_info, credentials, lp): - """Setup the templates database. - - :param path: Path to the database. - :param setup_path: Function for obtaining the path to setup files. - :param session_info: Session info - :param credentials: Credentials - :param lp: Loadparm context - """ - templates_ldb = SamDB(path, session_info=session_info, - credentials=credentials, lp=lp) - # Wipes the database - try: - templates_ldb.erase() - # This should be 'except LdbError', but on a re-provision the assert in ldb.erase fires, and we need to catch that too - except: - os.unlink(path) - - templates_ldb.load_ldif_file_add(setup_path("provision_templates_init.ldif")) - - templates_ldb = SamDB(path, session_info=session_info, - credentials=credentials, lp=lp) - - templates_ldb.load_ldif_file_add(setup_path("provision_templates.ldif")) - - -def setup_registry(path, setup_path, session_info, credentials, lp): +def setup_registry(path, setup_path, session_info, lp): """Setup the registry. :param path: Path to the registry database @@ -679,14 +825,14 @@ def setup_registry(path, setup_path, session_info, credentials, lp): """ reg = registry.Registry() hive = registry.open_ldb(path, session_info=session_info, - credentials=credentials, lp_ctx=lp) + lp_ctx=lp) reg.mount_hive(hive, 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, setup_path, session_info, credentials, lp): +def setup_idmapdb(path, setup_path, session_info, lp): """Setup the idmap database. :param path: path to the idmap database @@ -699,7 +845,7 @@ def setup_idmapdb(path, setup_path, session_info, credentials, lp): os.unlink(path) idmap_ldb = IDmapDB(path, session_info=session_info, - credentials=credentials, lp=lp) + lp=lp) idmap_ldb.erase() idmap_ldb.load_ldif_file_add(setup_path("idmap_init.ldif")) @@ -728,7 +874,7 @@ def setup_samdb_rootdse(samdb, setup_path, names): def setup_self_join(samdb, names, machinepass, dnspass, domainsid, invocationid, setup_path, - policyguid): + policyguid, policyguid_dc, domainControllerFunctionality): """Join a host to its own domain.""" assert isinstance(invocationid, str) setup_add_ldif(samdb, setup_path("provision_self_join.ldif"), { @@ -744,61 +890,116 @@ def setup_self_join(samdb, names, "DNSPASS_B64": b64encode(dnspass), "REALM": names.realm, "DOMAIN": names.domain, - "DNSDOMAIN": names.dnsdomain}) + "DNSDOMAIN": names.dnsdomain, + "SAMBA_VERSION_STRING": version, + "DOMAIN_CONTROLLER_FUNCTIONALITY": str(domainControllerFunctionality)}) + setup_add_ldif(samdb, setup_path("provision_group_policy.ldif"), { "POLICYGUID": policyguid, + "POLICYGUID_DC": policyguid_dc, "DNSDOMAIN": names.dnsdomain, "DOMAINSID": str(domainsid), "DOMAINDN": names.domaindn}) + + # add the NTDSGUID based SPNs + ntds_dn = "CN=NTDS Settings,CN=%s,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,%s" % (names.hostname, names.domaindn) + names.ntdsguid = samdb.searchone(basedn=ntds_dn, attribute="objectGUID", + expression="", scope=SCOPE_BASE) + assert isinstance(names.ntdsguid, str) + + # Setup fSMORoleOwner entries to point at the newly created DC entry + setup_modify_ldif(samdb, setup_path("provision_self_join_modify.ldif"), { + "DOMAIN": names.domain, + "DNSDOMAIN": names.dnsdomain, + "DOMAINDN": names.domaindn, + "CONFIGDN": names.configdn, + "SCHEMADN": names.schemadn, + "DEFAULTSITE": names.sitename, + "SERVERDN": names.serverdn, + "NETBIOSNAME": names.netbiosname, + "NTDSGUID": names.ntdsguid + }) def setup_samdb(path, setup_path, session_info, credentials, lp, names, message, - domainsid, aci, domainguid, policyguid, + domainsid, domainguid, policyguid, policyguid_dc, fill, adminpass, krbtgtpass, machinepass, invocationid, dnspass, - serverrole, ldap_backend=None, - ldap_backend_type=None): + serverrole, schema=None, ldap_backend=None): """Setup a complete SAM Database. :note: This will wipe the main SAM database file! """ - erase = (fill != FILL_DRS) + # Do NOT change these default values without discussion with the team and reslease manager. + domainFunctionality = DS_DOMAIN_FUNCTION_2008 + forestFunctionality = DS_DOMAIN_FUNCTION_2008 + domainControllerFunctionality = DS_DC_FUNCTION_2008 # Also wipes the database setup_samdb_partitions(path, setup_path, message=message, lp=lp, credentials=credentials, session_info=session_info, names=names, - ldap_backend=ldap_backend, serverrole=serverrole, - ldap_backend_type=ldap_backend_type, erase=erase) + ldap_backend=ldap_backend, serverrole=serverrole) - samdb = SamDB(path, session_info=session_info, - credentials=credentials, lp=lp) - if fill == FILL_DRS: - return samdb + if (schema == None): + schema = Schema(setup_path, domainsid, schemadn=names.schemadn, serverdn=names.serverdn, + sambadn=names.sambadn, ldap_backend_type=ldap_backend.ldap_backend_type) + + # Load the database, but importantly, use Ldb not SamDB as we don't want to load the global schema + samdb = Ldb(session_info=session_info, + credentials=credentials, lp=lp) message("Pre-loading the Samba 4 and AD schema") - samdb.set_domain_sid(str(domainsid)) - if serverrole == "domain controller": - samdb.set_invocation_id(invocationid) - load_schema(setup_path, samdb, names.schemadn, names.netbiosname, - names.configdn, names.sitename, names.serverdn, - names.hostname) + # Load the schema from the one we computed earlier + samdb.set_schema_from_ldb(schema.ldb) + + # And now we can connect to the DB - the schema won't be loaded from the DB + samdb.connect(path) + + # Load @OPTIONS + samdb.load_ldif_file_add(setup_path("provision_options.ldif")) + + if fill == FILL_DRS: + return samdb samdb.transaction_start() - try: - message("Adding DomainDN: %s (permitted to fail)" % names.domaindn) + message("Erasing data from partitions") + # Load the schema (again). This time it will force a reindex, + # and will therefore make the erase_partitions() below + # computationally sane + samdb.set_schema_from_ldb(schema.ldb) + samdb.erase_partitions() + + # 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)) + if serverrole == "domain controller": + samdb.set_invocation_id(invocationid) + + message("Adding DomainDN: %s" % names.domaindn) if serverrole == "domain controller": domain_oc = "domainDNS" else: domain_oc = "samba4LocalDomain" +#impersonate domain admin + admin_session_info = admin_session(lp, str(domainsid)) + samdb.set_session_info(admin_session_info) + setup_add_ldif(samdb, setup_path("provision_basedn.ldif"), { "DOMAINDN": names.domaindn, - "ACI": aci, "DOMAIN_OC": domain_oc }) @@ -809,7 +1010,7 @@ def setup_samdb(path, setup_path, session_info, credentials, lp, domainguid_mod = "" setup_modify_ldif(samdb, setup_path("provision_basedn_modify.ldif"), { - "LDAPTIME": timestring(int(time.time())), + "CREATTIME": str(int(time.time()) * 1e7), # seconds -> ticks "DOMAINSID": str(domainsid), "SCHEMADN": names.schemadn, "NETBIOSNAME": names.netbiosname, @@ -819,12 +1020,15 @@ def setup_samdb(path, setup_path, session_info, credentials, lp, "POLICYGUID": policyguid, "DOMAINDN": names.domaindn, "DOMAINGUID_MOD": domainguid_mod, + "DOMAIN_FUNCTIONALITY": str(domainFunctionality), + "SAMBA_VERSION_STRING": version }) - message("Adding configuration container (permitted to fail)") + message("Adding configuration container") + descr = get_config_descriptor(domainsid); setup_add_ldif(samdb, setup_path("provision_configuration_basedn.ldif"), { "CONFIGDN": names.configdn, - "ACI": aci, + "DESCRIPTOR": descr, }) message("Modifying configuration container") setup_modify_ldif(samdb, setup_path("provision_configuration_basedn_modify.ldif"), { @@ -832,31 +1036,12 @@ def setup_samdb(path, setup_path, session_info, credentials, lp, "SCHEMADN": names.schemadn, }) - message("Adding schema container (permitted to fail)") - setup_add_ldif(samdb, setup_path("provision_schema_basedn.ldif"), { - "SCHEMADN": names.schemadn, - "ACI": aci, - }) - message("Modifying schema container") - - prefixmap = open(setup_path("prefixMap.txt"), 'r').read() - - setup_modify_ldif(samdb, - setup_path("provision_schema_basedn_modify.ldif"), { - "SCHEMADN": names.schemadn, - "NETBIOSNAME": names.netbiosname, - "DEFAULTSITE": names.sitename, - "CONFIGDN": names.configdn, - "SERVERDN": names.serverdn, - "PREFIXMAP_B64": b64encode(prefixmap) - }) - - message("Setting up sam.ldb Samba4 schema") - setup_add_ldif(samdb, setup_path("schema_samba4.ldif"), - {"SCHEMADN": names.schemadn }) - message("Setting up sam.ldb AD schema") - setup_add_ldif(samdb, setup_path("schema.ldif"), - {"SCHEMADN": names.schemadn}) + # The LDIF here was created when the Schema object was constructed + message("Setting up sam.ldb schema") + samdb.add_ldif(schema.schema_dn_add) + samdb.modify_ldif(schema.schema_dn_modify) + samdb.write_prefixes_from_schema() + samdb.add_ldif(schema.schema_data) setup_add_ldif(samdb, setup_path("aggregate_schema.ldif"), {"SCHEMADN": names.schemadn}) @@ -869,20 +1054,23 @@ def setup_samdb(path, setup_path, session_info, credentials, lp, "DOMAIN": names.domain, "SCHEMADN": names.schemadn, "DOMAINDN": names.domaindn, - "SERVERDN": names.serverdn + "SERVERDN": names.serverdn, + "FOREST_FUNCTIONALALITY": str(forestFunctionality) }) message("Setting up display specifiers") - setup_add_ldif(samdb, setup_path("display_specifiers.ldif"), - {"CONFIGDN": names.configdn}) + 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) - message("Adding users container (permitted to fail)") + message("Adding users container") setup_add_ldif(samdb, setup_path("provision_users_add.ldif"), { "DOMAINDN": names.domaindn}) message("Modifying users container") setup_modify_ldif(samdb, setup_path("provision_users_modify.ldif"), { "DOMAINDN": names.domaindn}) - message("Adding computers container (permitted to fail)") + message("Adding computers container") setup_add_ldif(samdb, setup_path("provision_computers_add.ldif"), { "DOMAINDN": names.domaindn}) message("Modifying computers container") @@ -890,11 +1078,13 @@ def setup_samdb(path, setup_path, session_info, credentials, lp, "DOMAINDN": names.domaindn}) message("Setting up sam.ldb data") setup_add_ldif(samdb, setup_path("provision.ldif"), { + "CREATTIME": str(int(time.time()) * 1e7), # seconds -> ticks "DOMAINDN": names.domaindn, "NETBIOSNAME": names.netbiosname, "DEFAULTSITE": names.sitename, "CONFIGDN": names.configdn, - "SERVERDN": names.serverdn + "SERVERDN": names.serverdn, + "POLICYGUID_DC": policyguid_dc }) if fill == FILL_FULL: @@ -913,7 +1103,14 @@ def setup_samdb(path, setup_path, session_info, credentials, lp, dnspass=dnspass, machinepass=machinepass, domainsid=domainsid, policyguid=policyguid, - setup_path=setup_path) + policyguid_dc=policyguid_dc, + setup_path=setup_path, + domainControllerFunctionality=domainControllerFunctionality) + + ntds_dn = "CN=NTDS Settings,CN=%s,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,%s" % (names.hostname, names.domaindn) + names.ntdsguid = samdb.searchone(basedn=ntds_dn, + attribute="objectGUID", expression="", scope=SCOPE_BASE) + assert isinstance(names.ntdsguid, str) except: samdb.transaction_cancel() @@ -927,29 +1124,46 @@ FILL_FULL = "FULL" FILL_NT4SYNC = "NT4SYNC" FILL_DRS = "DRS" + def provision(setup_dir, message, session_info, - credentials, smbconf=None, targetdir=None, samdb_fill=FILL_FULL, realm=None, + 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, adminpass=None, krbtgtpass=None, domainguid=None, - policyguid=None, invocationid=None, machinepass=None, - dnspass=None, root=None, nobody=None, nogroup=None, users=None, + domainsid=None, adminpass=None, ldapadminpass=None, + krbtgtpass=None, domainguid=None, + policyguid=None, policyguid_dc=None, invocationid=None, + machinepass=None, + dnspass=None, root=None, nobody=None, users=None, wheel=None, backup=None, aci=None, serverrole=None, - ldap_backend=None, ldap_backend_type=None, sitename=None): + ldap_backend_extra_port=None, ldap_backend_type=None, + sitename=None, + ol_mmr_urls=None, ol_olc=None, + setup_ds_path=None, slapd_path=None, nosync=False, + ldap_dryrun_mode=False): """Provision samba4 :note: caution, this wipes all existing data! """ def setup_path(file): - return os.path.join(setup_dir, file) + return os.path.join(setup_dir, file) if domainsid is None: - domainsid = security.random_sid() + domainsid = security.random_sid() + else: + domainsid = security.dom_sid(domainsid) + + # create/adapt the group policy GUIDs if policyguid is None: policyguid = str(uuid.uuid4()) + policyguid = policyguid.upper() + if policyguid_dc is None: + policyguid_dc = str(uuid.uuid4()) + policyguid_dc = policyguid_dc.upper() + if adminpass is None: adminpass = glue.generate_random_str(12) if krbtgtpass is None: @@ -958,6 +1172,11 @@ def provision(setup_dir, message, session_info, machinepass = glue.generate_random_str(12) if dnspass is None: dnspass = glue.generate_random_str(12) + if ldapadminpass is None: + #Make a new, random password between Samba and it's LDAP server + ldapadminpass=glue.generate_random_str(12) + + root_uid = findnss_uid([root or "root"]) nobody_uid = findnss_uid([nobody or "nobody"]) users_gid = findnss_gid([users or "users"]) @@ -965,8 +1184,6 @@ def provision(setup_dir, message, session_info, wheel_gid = findnss_gid(["wheel", "adm"]) else: wheel_gid = findnss_gid([wheel]) - if aci is None: - aci = "# no aci for local ldb" if targetdir is not None: if (not os.path.exists(os.path.join(targetdir, "etc"))): @@ -1014,11 +1231,32 @@ def provision(setup_dir, message, session_info, ldapi_url = "ldapi://%s" % urllib.quote(paths.s4_ldapi_path, safe="") - if ldap_backend is not None: - if ldap_backend == "ldapi": - # provision-backend will set this path suggested slapd command line / fedorads.inf - ldap_backend = "ldapi://%s" % urllib.quote(os.path.join(paths.private_dir, "ldap", "ldapi"), safe="") - + schema = Schema(setup_path, domainsid, schemadn=names.schemadn, serverdn=names.serverdn, + sambadn=names.sambadn, ldap_backend_type=ldap_backend_type) + + secrets_credentials = credentials + provision_backend = None + if ldap_backend_type: + # We only support an LDAP backend over ldapi:// + + provision_backend = ProvisionBackend(paths=paths, setup_path=setup_path, + lp=lp, credentials=credentials, + names=names, + message=message, hostname=hostname, + root=root, schema=schema, + ldap_backend_type=ldap_backend_type, + ldapadminpass=ldapadminpass, + ldap_backend_extra_port=ldap_backend_extra_port, + ol_mmr_urls=ol_mmr_urls, + slapd_path=slapd_path, + setup_ds_path=setup_ds_path, + ldap_dryrun_mode=ldap_dryrun_mode) + + # Now use the backend credentials to access the databases + credentials = provision_backend.credentials + secrets_credentials = provision_backend.adminCredentials + ldapi_url = provision_backend.ldapi_uri + # only install a new shares config db if there is none if not os.path.exists(paths.shareconf): message("Setting up share.ldb") @@ -1030,33 +1268,30 @@ def provision(setup_dir, message, session_info, message("Setting up secrets.ldb") secrets_ldb = setup_secretsdb(paths.secrets, setup_path, session_info=session_info, - credentials=credentials, lp=lp) + credentials=secrets_credentials, lp=lp) message("Setting up the registry") setup_registry(paths.hklm, setup_path, session_info, - credentials=credentials, lp=lp) - - message("Setting up templates db") - setup_templatesdb(paths.templates, setup_path, session_info=session_info, - credentials=credentials, lp=lp) + lp=lp) message("Setting up idmap db") idmap = setup_idmapdb(paths.idmapdb, setup_path, session_info=session_info, - credentials=credentials, lp=lp) + lp=lp) + message("Setting up SAM db") samdb = setup_samdb(paths.samdb, setup_path, session_info=session_info, credentials=credentials, lp=lp, names=names, message=message, domainsid=domainsid, - aci=aci, domainguid=domainguid, policyguid=policyguid, + schema=schema, domainguid=domainguid, + policyguid=policyguid, policyguid_dc=policyguid_dc, fill=samdb_fill, adminpass=adminpass, krbtgtpass=krbtgtpass, invocationid=invocationid, machinepass=machinepass, dnspass=dnspass, - serverrole=serverrole, ldap_backend=ldap_backend, - ldap_backend_type=ldap_backend_type) + serverrole=serverrole, ldap_backend=provision_backend) - if lp.get("server role") == "domain controller": + if serverrole == "domain controller": if paths.netlogon is None: message("Existing smb.conf does not have a [netlogon] share, but you are configuring a DC.") message("Please either remove %s or see the template at %s" % @@ -1069,12 +1304,24 @@ def provision(setup_dir, message, session_info, (paths.smbconf, setup_path("provision.smb.conf.dc"))) assert(paths.sysvol is not None) - policy_path = os.path.join(paths.sysvol, names.dnsdomain, "Policies", + # Set up group policies (domain policy and domain controller policy) + + policy_path = os.path.join(paths.sysvol, names.dnsdomain, "Policies", "{" + policyguid + "}") os.makedirs(policy_path, 0755) - open(os.path.join(policy_path, "GPT.INI"), 'w').write("") - os.makedirs(os.path.join(policy_path, "Machine"), 0755) - os.makedirs(os.path.join(policy_path, "User"), 0755) + open(os.path.join(policy_path, "GPT.INI"), 'w').write( + "[General]\r\nVersion=65543") + os.makedirs(os.path.join(policy_path, "MACHINE"), 0755) + os.makedirs(os.path.join(policy_path, "USER"), 0755) + + policy_path_dc = os.path.join(paths.sysvol, names.dnsdomain, "Policies", + "{" + policyguid_dc + "}") + os.makedirs(policy_path_dc, 0755) + open(os.path.join(policy_path_dc, "GPT.INI"), 'w').write( + "[General]\r\nVersion=2") + os.makedirs(os.path.join(policy_path_dc, "MACHINE"), 0755) + os.makedirs(os.path.join(policy_path_dc, "USER"), 0755) + if not os.path.isdir(paths.netlogon): os.makedirs(paths.netlogon, 0755) @@ -1088,29 +1335,27 @@ def provision(setup_dir, message, session_info, # Only make a zone file on the first DC, it should be replicated with DNS replication if serverrole == "domain controller": - secrets_ldb = Ldb(paths.secrets, session_info=session_info, - credentials=credentials, lp=lp) - secretsdb_become_dc(secrets_ldb, setup_path, domain=domain, realm=names.realm, - netbiosname=names.netbiosname, domainsid=domainsid, - keytab_path=paths.keytab, samdb_url=paths.samdb, - dns_keytab_path=paths.dns_keytab, dnspass=dnspass, - machinepass=machinepass, dnsdomain=names.dnsdomain) - - samdb = SamDB(paths.samdb, session_info=session_info, - credentials=credentials, lp=lp) + secretsdb_self_join(secrets_ldb, domain=domain, + realm=names.realm, + dnsdomain=names.dnsdomain, + netbiosname=names.netbiosname, + domainsid=domainsid, + machinepass=machinepass, + secure_channel_type=SEC_CHAN_BDC) + + secretsdb_setup_dns(secrets_ldb, setup_path, + realm=names.realm, dnsdomain=names.dnsdomain, + dns_keytab_path=paths.dns_keytab, + dnspass=dnspass) domainguid = samdb.searchone(basedn=domaindn, attribute="objectGUID") assert isinstance(domainguid, str) - hostguid = samdb.searchone(basedn=domaindn, attribute="objectGUID", - expression="(&(objectClass=computer)(cn=%s))" % names.hostname, - scope=SCOPE_SUBTREE) - assert isinstance(hostguid, str) create_zone_file(paths.dns, setup_path, dnsdomain=names.dnsdomain, domaindn=names.domaindn, hostip=hostip, hostip6=hostip6, hostname=names.hostname, dnspass=dnspass, realm=names.realm, - domainguid=domainguid, hostguid=hostguid) + domainguid=domainguid, ntdsguid=names.ntdsguid) create_named_conf(paths.namedconf, setup_path, realm=names.realm, dnsdomain=names.dnsdomain, private_dir=paths.private_dir) @@ -1121,24 +1366,84 @@ def provision(setup_dir, message, session_info, message("See %s for an example configuration include file for BIND" % paths.namedconf) message("and %s for further documentation required for secure DNS updates" % paths.namedtxt) - create_krb5_conf(paths.krb5conf, setup_path, dnsdomain=names.dnsdomain, - hostname=names.hostname, realm=names.realm) + create_krb5_conf(paths.krb5conf, setup_path, + dnsdomain=names.dnsdomain, hostname=names.hostname, + realm=names.realm) message("A Kerberos configuration suitable for Samba 4 has been generated at %s" % paths.krb5conf) + #Now commit the secrets.ldb to disk + secrets_ldb.transaction_commit() + + if provision_backend is not None: + if ldap_backend_type == "fedora-ds": + ldapi_db = Ldb(provision_backend.ldapi_uri, lp=lp, credentials=credentials) + + # delete default SASL mappings + res = ldapi_db.search(expression="(!(cn=samba-admin mapping))", base="cn=mapping,cn=sasl,cn=config", scope=SCOPE_ONELEVEL, attrs=["dn"]) + + # configure in-directory access control on Fedora DS via the aci attribute (over a direct ldapi:// socket) + for i in range (0, len(res)): + dn = str(res[i]["dn"]) + ldapi_db.delete(dn) + + aci = """(targetattr = "*") (version 3.0;acl "full access to all by samba-admin";allow (all)(userdn = "ldap:///CN=samba-admin,%s");)""" % names.sambadn + + m = ldb.Message() + m["aci"] = ldb.MessageElement([aci], ldb.FLAG_MOD_REPLACE, "aci") + + m.dn = ldb.Dn(1, names.domaindn) + ldapi_db.modify(m) + + m.dn = ldb.Dn(1, names.configdn) + ldapi_db.modify(m) + + m.dn = ldb.Dn(1, names.schemadn) + ldapi_db.modify(m) + + # if an LDAP backend is in use, terminate slapd after final provision and check its proper termination + if provision_backend.slapd.poll() is None: + #Kill the slapd + if hasattr(provision_backend.slapd, "terminate"): + provision_backend.slapd.terminate() + else: + # Older python versions don't have .terminate() + import signal + os.kill(provision_backend.slapd.pid, signal.SIGTERM) + + #and now wait for it to die + provision_backend.slapd.communicate() + + # now display slapd_command_file.txt to show how slapd must be started next time + message("Use later the following commandline to start slapd, then Samba:") + slapd_command = "\'" + "\' \'".join(provision_backend.slapd_command) + "\'" + message(slapd_command) + message("This slapd-Commandline is also stored under: " + paths.ldapdir + "/ldap_backend_startup.sh") + + setup_file(setup_path("ldap_backend_startup.sh"), paths.ldapdir + "/ldap_backend_startup.sh", { + "SLAPD_COMMAND" : slapd_command}) + + create_phpldapadmin_config(paths.phpldapadminconfig, setup_path, ldapi_url) message("Please install the phpLDAPadmin configuration located at %s into /etc/phpldapadmin/config.php" % paths.phpldapadminconfig) message("Once the above files are installed, your Samba4 server will be ready to use") - message("Server Role: %s" % serverrole) - message("Hostname: %s" % names.hostname) - message("NetBIOS Domain: %s" % names.domain) - message("DNS Domain: %s" % names.dnsdomain) - message("DOMAIN SID: %s" % str(domainsid)) + message("Server Role: %s" % serverrole) + message("Hostname: %s" % names.hostname) + message("NetBIOS Domain: %s" % names.domain) + message("DNS Domain: %s" % names.dnsdomain) + message("DOMAIN SID: %s" % str(domainsid)) if samdb_fill == FILL_FULL: - message("Admin password: %s" % adminpass) + message("Admin password: %s" % adminpass) + if provision_backend: + if provision_backend.credentials.get_bind_dn() is not None: + message("LDAP Backend Admin DN: %s" % provision_backend.credentials.get_bind_dn()) + else: + message("LDAP Admin User: %s" % provision_backend.credentials.get_username()) + message("LDAP Admin Password: %s" % provision_backend.credentials.get_password()) + result = ProvisionResult() result.domaindn = domaindn result.paths = paths @@ -1147,26 +1452,34 @@ def provision(setup_dir, message, session_info, return result + def provision_become_dc(setup_dir=None, smbconf=None, targetdir=None, realm=None, - rootdn=None, domaindn=None, schemadn=None, configdn=None, - serverdn=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, invocationid=None, machinepass=None, - dnspass=None, root=None, nobody=None, nogroup=None, users=None, - wheel=None, backup=None, aci=None, serverrole=None, - ldap_backend=None, ldap_backend_type=None, sitename=None): + policyguid=None, policyguid_dc=None, invocationid=None, + machinepass=None, + dnspass=None, root=None, nobody=None, users=None, + wheel=None, backup=None, serverrole=None, + ldap_backend=None, ldap_backend_type=None, + sitename=None, debuglevel=1): def message(text): """print a message if quiet is not set.""" print text + glue.set_debug_level(debuglevel) + return provision(setup_dir, message, 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="127.0.0.1", domainsid=domainsid, machinepass=machinepass, serverrole="domain controller", sitename=sitename) - + 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="127.0.0.1", domainsid=domainsid, + machinepass=machinepass, serverrole="domain controller", + sitename=sitename) + def setup_db_config(setup_path, dbdir): """Setup a Berkeley database. @@ -1175,361 +1488,464 @@ def setup_db_config(setup_path, dbdir): :param dbdir: Database directory.""" 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) - + 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}) +class ProvisionBackend(object): + def __init__(self, paths=None, setup_path=None, lp=None, credentials=None, + names=None, message=None, + hostname=None, root=None, + schema=None, ldapadminpass=None, + ldap_backend_type=None, ldap_backend_extra_port=None, + ol_mmr_urls=None, + setup_ds_path=None, slapd_path=None, + nosync=False, ldap_dryrun_mode=False): + """Provision an LDAP backend for samba4 + + This works for OpenLDAP and Fedora DS + """ + self.ldapi_uri = "ldapi://" + urllib.quote(os.path.join(paths.ldapdir, "ldapi"), safe="") + + if not os.path.isdir(paths.ldapdir): + os.makedirs(paths.ldapdir, 0700) + + if ldap_backend_type == "existing": + #Check to see that this 'existing' LDAP backend in fact exists + ldapi_db = Ldb(self.ldapi_uri, credentials=credentials) + search_ol_rootdse = 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.credentials = credentials + self.ldap_backend_type = "openldap" #For now, assume existing backends at least emulate OpenLDAP + return + + # we will shortly start slapd with ldapi for final provisioning. first check with ldapsearch -> rootDSE via self.ldapi_uri + # if another instance of slapd is already running + try: + ldapi_db = Ldb(self.ldapi_uri) + search_ol_rootdse = ldapi_db.search(base="", scope=SCOPE_BASE, + expression="(objectClass=OpenLDAProotDSE)"); + try: + f = open(paths.slapdpid, "r") + p = f.read() + f.close() + message("Check for slapd Process with PID: " + str(p) + " and terminate it manually.") + except: + pass + + raise ProvisioningError("Warning: Another slapd Instance seems already running on this host, listening to " + self.ldapi_uri + ". Please shut it down before you continue. ") + + except LdbError, e: + pass -def provision_backend(setup_dir=None, message=None, - smbconf=None, targetdir=None, realm=None, - rootdn=None, domaindn=None, schemadn=None, configdn=None, - domain=None, hostname=None, adminpass=None, root=None, serverrole=None, - ldap_backend_type=None, ldap_backend_port=None, - ol_mmr_urls=None,ol_olc=None,ol_slaptest=None): - - def setup_path(file): - return os.path.join(setup_dir, file) - - if hostname is None: - hostname = socket.gethostname().split(".")[0].lower() - - if root is None: - root = findnss(pwd.getpwnam, ["root"])[0] - - if adminpass is None: - adminpass = glue.generate_random_str(12) - - if targetdir is not None: - if (not os.path.exists(os.path.join(targetdir, "etc"))): - os.makedirs(os.path.join(targetdir, "etc")) - smbconf = os.path.join(targetdir, "etc", "smb.conf") - elif smbconf is None: - smbconf = param.default_path() - assert smbconf is not None - - # only install a new smb.conf if there isn't one there already - if not os.path.exists(smbconf): - make_smbconf(smbconf, setup_path, hostname, domain, realm, serverrole, - targetdir) - - # openldap-online-configuration: validation of olc and slaptest - if ol_olc == "yes" and ol_slaptest is None: - sys.exit("Warning: OpenLDAP-Online-Configuration cant be setup without path to slaptest-Binary!") - - if ol_olc == "yes" and ol_slaptest is not None: - ol_slaptest = ol_slaptest + "/slaptest" - if not os.path.exists(ol_slaptest): - message (ol_slaptest) - sys.exit("Warning: Given Path to slaptest-Binary does not exist!") - ### - - - - lp = param.LoadParm() - lp.load(smbconf) + # Try to print helpful messages when the user has not specified the path to slapd + if 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(slapd_path): + message (slapd_path) + raise ProvisioningError("Warning: Given Path to slapd does not exist!") - if serverrole is None: - serverrole = lp.get("server role") - - names = guess_names(lp=lp, hostname=hostname, domain=domain, - dnsdomain=realm, serverrole=serverrole, - rootdn=rootdn, domaindn=domaindn, configdn=configdn, - schemadn=schemadn) + schemadb_path = os.path.join(paths.ldapdir, "schema-tmp.ldb") + try: + os.unlink(schemadb_path) + except OSError: + pass - paths = provision_paths_from_lp(lp, names.dnsdomain) - if not os.path.isdir(paths.ldapdir): - os.makedirs(paths.ldapdir, 0700) - schemadb_path = os.path.join(paths.ldapdir, "schema-tmp.ldb") - try: - os.unlink(schemadb_path) - except OSError: - pass + # 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 + os.path.join(paths.ldapdir, "schema-tmp.ldb") + schema.ldb.connect(schemadb_path) + schema.ldb.transaction_start() + + # These bits of LDIF are supplied when the Schema object is created + schema.ldb.add_ldif(schema.schema_dn_add) + schema.ldb.modify_ldif(schema.schema_dn_modify) + schema.ldb.add_ldif(schema.schema_data) + schema.ldb.transaction_commit() + + self.credentials = Credentials() + self.credentials.guess(lp) + #Kerberos to an ldapi:// backend makes no sense + self.credentials.set_kerberos_state(DONT_USE_KERBEROS) + + self.adminCredentials = Credentials() + self.adminCredentials.guess(lp) + #Kerberos to an ldapi:// backend makes no sense + self.adminCredentials.set_kerberos_state(DONT_USE_KERBEROS) + + self.ldap_backend_type = ldap_backend_type + + if ldap_backend_type == "fedora-ds": + provision_fds_backend(self, paths=paths, setup_path=setup_path, + names=names, message=message, + hostname=hostname, + ldapadminpass=ldapadminpass, root=root, + schema=schema, + ldap_backend_extra_port=ldap_backend_extra_port, + setup_ds_path=setup_ds_path, + slapd_path=slapd_path, + nosync=nosync, + ldap_dryrun_mode=ldap_dryrun_mode) + + elif ldap_backend_type == "openldap": + provision_openldap_backend(self, paths=paths, setup_path=setup_path, + names=names, message=message, + hostname=hostname, + ldapadminpass=ldapadminpass, root=root, + schema=schema, + ldap_backend_extra_port=ldap_backend_extra_port, + ol_mmr_urls=ol_mmr_urls, + slapd_path=slapd_path, + nosync=nosync, + ldap_dryrun_mode=ldap_dryrun_mode) + else: + raise ProvisioningError("Unknown LDAP backend type selected") - schemadb = Ldb(schemadb_path, lp=lp) - - prefixmap = open(setup_path("prefixMap.txt"), 'r').read() + self.credentials.set_password(ldapadminpass) + self.adminCredentials.set_username("samba-admin") + self.adminCredentials.set_password(ldapadminpass) - setup_add_ldif(schemadb, setup_path("provision_schema_basedn.ldif"), - {"SCHEMADN": names.schemadn, - "ACI": "#", - }) - setup_modify_ldif(schemadb, - setup_path("provision_schema_basedn_modify.ldif"), \ - {"SCHEMADN": names.schemadn, - "NETBIOSNAME": names.netbiosname, - "DEFAULTSITE": DEFAULTSITE, - "CONFIGDN": names.configdn, - "SERVERDN": names.serverdn, - "PREFIXMAP_B64": b64encode(prefixmap) - }) + # 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) - setup_add_ldif(schemadb, setup_path("schema_samba4.ldif"), - {"SCHEMADN": names.schemadn }) - setup_add_ldif(schemadb, setup_path("schema.ldif"), - {"SCHEMADN": names.schemadn}) - - if ldap_backend_type == "fedora-ds": - if ldap_backend_port is not None: - serverport = "ServerPort=%d" % ldap_backend_port - else: - serverport = "" - - setup_file(setup_path("fedorads.inf"), paths.fedoradsinf, - {"ROOT": root, - "HOSTNAME": hostname, - "DNSDOMAIN": names.dnsdomain, - "LDAPDIR": paths.ldapdir, - "DOMAINDN": names.domaindn, - "LDAPMANAGERDN": names.ldapmanagerdn, - "LDAPMANAGERPASS": adminpass, - "SERVERPORT": serverport}) + while self.slapd.poll() is None: + # Wait until the socket appears + try: + ldapi_db = Ldb(self.ldapi_uri, lp=lp, credentials=self.credentials) + search_ol_rootdse = 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, e: + time.sleep(1) + pass - setup_file(setup_path("fedorads-partitions.ldif"), paths.fedoradspartitions, - {"CONFIGDN": names.configdn, - "SCHEMADN": names.schemadn, - }) - - mapping = "schema-map-fedora-ds-1.0" - backend_schema = "99_ad.ldif" + raise ProvisioningError("slapd died before we could make a connection to it") + + +def provision_openldap_backend(result, paths=None, setup_path=None, names=None, + message=None, + hostname=None, ldapadminpass=None, root=None, + schema=None, + ldap_backend_extra_port=None, + ol_mmr_urls=None, + slapd_path=None, nosync=False, + ldap_dryrun_mode=False): + + #Allow the test scripts to turn off fsync() for OpenLDAP as for TDB and LDB + nosync_config = "" + if nosync: + nosync_config = "dbnosync" - slapdcommand="Initialise Fedora DS with: setup-ds.pl --file=%s" % paths.fedoradsinf - - ldapuser = "--simple-bind-dn=" + names.ldapmanagerdn - - elif ldap_backend_type == "openldap": - attrs = ["linkID", "lDAPDisplayName"] - res = schemadb.search(expression="(&(linkID=*)(!(linkID:1.2.840.113556.1.4.803:=1))(objectclass=attributeSchema)(attributeSyntax=2.5.5.1))", base=names.schemadn, scope=SCOPE_SUBTREE, attrs=attrs) - - memberof_config = "# Generated from schema in %s\n" % schemadb_path - refint_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 = schemadb.searchone(basedn=names.schemadn, - expression=expression, - attribute="lDAPDisplayName", - scope=SCOPE_SUBTREE) - if target is not None: - refint_attributes = refint_attributes + " " + target + " " + res[i]["lDAPDisplayName"][0] + lnkattr = get_linked_attributes(names.schemadn,schema.ldb) + 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" : str(res[i]["lDAPDisplayName"][0]), - "MEMBEROF_ATTR" : str(target) }) - - refint_config = read_and_sub_file(setup_path("refint.conf"), - { "LINK_ATTRS" : refint_attributes}) + 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 = schema.ldb.search(expression="(&(objectclass=attributeSchema)(searchFlags:1.2.840.113556.1.4.803:=1))", base=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 = "" + mmr_on_config = "" + mmr_replicator_acl = "" + mmr_serverids_config = "" + mmr_syncrepl_schema_config = "" + mmr_syncrepl_config_config = "" + mmr_syncrepl_user_config = "" - - if ol_mmr_urls is not None: - # For now, make these equal - mmr_pass = adminpass - - url_list=filter(None,ol_mmr_urls.split(' ')) - if (len(url_list) == 1): - url_list=filter(None,ol_mmr_urls.split(',')) + + if ol_mmr_urls is not None: + # For now, make these equal + mmr_pass = ldapadminpass + + url_list=filter(None,ol_mmr_urls.split(' ')) + if (len(url_list) == 1): + url_list=filter(None,ol_mmr_urls.split(',')) - - 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": 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": 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": names.domaindn, - "LDAPSERVER" : url, - "MMR_PASSWORD": mmr_pass }) - # olc = yes? - olc_config_pass = "" - olc_config_acl = "" - olc_syncrepl_config = "" - olc_mmr_config = "" - if ol_olc == "yes": - olc_config_pass += read_and_sub_file(setup_path("olc_pass.conf"), - { "OLC_PW": adminpass }) - olc_config_acl += read_and_sub_file(setup_path("olc_acl.conf"),{}) + + 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": 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": names.configdn, + "LDAPSERVER" : url, + "MMR_PASSWORD": mmr_pass}) - # if olc = yes + mmr = yes, generate cn=config-replication directives - # and olc_seed.lif for the other mmr-servers - if ol_olc == "yes" and ol_mmr_urls is not None: - serverid=0 - olc_serverids_config = "" - olc_syncrepl_config = "" - olc_syncrepl_seed_config = "" - olc_mmr_config = "" - olc_mmr_config += read_and_sub_file(setup_path("olc_mmr.conf"),{}) - rid=1000 - 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": adminpass}) - - 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"), paths.olcseedldif, - {"OLC_SERVER_ID_CONF": olc_serverids_config, - "OLC_PW": adminpass, - "OLC_SYNCREPL_CONF": olc_syncrepl_seed_config}) + rid=rid+1 + mmr_syncrepl_user_config += read_and_sub_file(setup_path("mmr_syncrepl.conf"), + { "RID" : str(rid), + "MMRDN": 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 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=1000 + 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"), paths.olcseedldif, + {"OLC_SERVER_ID_CONF": olc_serverids_config, + "OLC_PW": ldapadminpass, + "OLC_SYNCREPL_CONF": olc_syncrepl_seed_config}) + # end olc + + setup_file(setup_path("slapd.conf"), paths.slapdconf, + {"DNSDOMAIN": names.dnsdomain, + "LDAPDIR": paths.ldapdir, + "DOMAINDN": names.domaindn, + "CONFIGDN": names.configdn, + "SCHEMADN": 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}) - - # end olc - - setup_file(setup_path("slapd.conf"), paths.slapdconf, - {"DNSDOMAIN": names.dnsdomain, - "LDAPDIR": paths.ldapdir, - "DOMAINDN": names.domaindn, - "CONFIGDN": names.configdn, - "SCHEMADN": 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_CONFIG_PASS": olc_config_pass, - "OLC_SYNCREPL_CONFIG": olc_syncrepl_config, - "OLC_CONFIG_ACL": olc_config_acl, - "OLC_MMR_CONFIG": olc_mmr_config, - "REFINT_CONFIG": refint_config}) - setup_file(setup_path("modules.conf"), paths.modulesconf, - {"REALM": names.realm}) + setup_db_config(setup_path, os.path.join(paths.ldapdir, "db", "user")) + setup_db_config(setup_path, os.path.join(paths.ldapdir, "db", "config")) + setup_db_config(setup_path, os.path.join(paths.ldapdir, "db", "schema")) + + if not os.path.exists(os.path.join(paths.ldapdir, "db", "samba", "cn=samba")): + os.makedirs(os.path.join(paths.ldapdir, "db", "samba", "cn=samba"), 0700) - setup_db_config(setup_path, os.path.join(paths.ldapdir, "db", "user")) - setup_db_config(setup_path, os.path.join(paths.ldapdir, "db", "config")) - setup_db_config(setup_path, os.path.join(paths.ldapdir, "db", "schema")) - - if not os.path.exists(os.path.join(paths.ldapdir, "db", "samba", "cn=samba")): - os.makedirs(os.path.join(paths.ldapdir, "db", "samba", "cn=samba"), 0700) - - setup_file(setup_path("cn=samba.ldif"), - os.path.join(paths.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(paths.ldapdir, "db", "samba", "cn=samba", "cn=samba-admin.ldif"), - {"LDAPADMINPASS_B64": b64encode(adminpass), - "UUID": str(uuid.uuid4()), - "LDAPTIME": timestring(int(time.time()))} ) + setup_file(setup_path("cn=samba.ldif"), + os.path.join(paths.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(paths.ldapdir, "db", "samba", "cn=samba", "cn=samba-admin.ldif"), + {"LDAPADMINPASS_B64": b64encode(ldapadminpass), + "UUID": str(uuid.uuid4()), + "LDAPTIME": timestring(int(time.time()))} ) + + if ol_mmr_urls is not None: + setup_file(setup_path("cn=replicator.ldif"), + os.path.join(paths.ldapdir, "db", "samba", "cn=samba", "cn=replicator.ldif"), + {"MMR_PASSWORD_B64": b64encode(mmr_pass), + "UUID": str(uuid.uuid4()), + "LDAPTIME": timestring(int(time.time()))} ) - if ol_mmr_urls is not None: - setup_file(setup_path("cn=replicator.ldif"), - os.path.join(paths.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" - - ldapi_uri = "ldapi://" + urllib.quote(os.path.join(paths.private_dir, "ldap", "ldapi"), safe="") - if ldap_backend_port is not None: - server_port_string = " -h ldap://0.0.0.0:%d" % ldap_backend_port + mapping = "schema-map-openldap-2.3" + backend_schema = "backend-schema.schema" + + backend_schema_data = schema.ldb.convert_schema_to_openldap("openldap", open(setup_path(mapping), 'r').read()) + assert backend_schema_data is not None + open(os.path.join(paths.ldapdir, backend_schema), 'w').write(backend_schema_data) + + # now we generate the needed strings to start slapd automatically, + # first ldapi_uri... + if 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 ol_mmr_urls is None: + server_port_string = "ldap://0.0.0.0:%d" % ldap_backend_extra_port else: - server_port_string = "" + server_port_string = "ldap://" + names.hostname + "." + names.dnsdomain +":%d" % ldap_backend_extra_port + else: + server_port_string = "" - if ol_olc != "yes" and ol_mmr_urls is None: - slapdcommand="Start slapd with: slapd -f " + paths.ldapdir + "/slapd.conf -h " + ldapi_uri + server_port_string + # Prepare the 'result' information - the commands to return in particular + result.slapd_provision_command = [slapd_path] - if ol_olc == "yes" and ol_mmr_urls is None: - slapdcommand="Start slapd with: slapd -F " + paths.olcdir + " -h \"" + ldapi_uri + " ldap://<FQHN>:<PORT>\"" + result.slapd_provision_command.append("-F" + paths.olcdir) - if ol_olc != "yes" and ol_mmr_urls is not None: - slapdcommand="Start slapd with: slapd -f " + paths.ldapdir + "/slapd.conf -h \"" + ldapi_uri + " ldap://<FQHN>:<PORT>\"" + result.slapd_provision_command.append("-h") - if ol_olc == "yes" and ol_mmr_urls is not None: - slapdcommand="Start slapd with: slapd -F " + paths.olcdir + " -h \"" + ldapi_uri + " ldap://<FQHN>:<PORT>\"" + # copy this command so we have two version, one with -d0 and only ldapi, and one with all the listen commands + result.slapd_command = list(result.slapd_provision_command) + + result.slapd_provision_command.append(result.ldapi_uri) + result.slapd_provision_command.append("-d0") + uris = result.ldapi_uri + if server_port_string is not "": + uris = uris + " " + server_port_string - ldapuser = "--username=samba-admin" + result.slapd_command.append(uris) - - schema_command = "bin/ad2oLschema --option=convert:target=" + ldap_backend_type + " -I " + setup_path(mapping) + " -H tdb://" + schemadb_path + " -O " + os.path.join(paths.ldapdir, backend_schema) - - os.system(schema_command) + # Set the username - done here because Fedora DS still uses the admin DN and simple bind + result.credentials.set_username("samba-admin") + + # 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 ldap_dryrun_mode: + sys.exit(0) - message("Your %s Backend for Samba4 is now configured, and is ready to be started" % ldap_backend_type) - message("Server Role: %s" % serverrole) - message("Hostname: %s" % names.hostname) - message("DNS Domain: %s" % names.dnsdomain) - message("Base DN: %s" % names.domaindn) + # Finally, convert the configuration into cn=config style! + if not os.path.isdir(paths.olcdir): + os.makedirs(paths.olcdir, 0770) - if ldap_backend_type == "openldap": - message("LDAP admin user: samba-admin") + retcode = subprocess.call([slapd_path, "-Ttest", "-f", paths.slapdconf, "-F", paths.olcdir], close_fds=True, shell=False) + +# We can't do this, as OpenLDAP is strange. It gives an error +# output to the above, but does the conversion sucessfully... +# +# if retcode != 0: +# raise ProvisioningError("conversion from slapd.conf to cn=config failed") + + if not os.path.exists(os.path.join(paths.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(paths.slapdconf) + + +def provision_fds_backend(result, paths=None, setup_path=None, names=None, + message=None, + hostname=None, ldapadminpass=None, root=None, + schema=None, + ldap_backend_extra_port=None, + setup_ds_path=None, + slapd_path=None, + nosync=False, + ldap_dryrun_mode=False): + + if ldap_backend_extra_port is not None: + serverport = "ServerPort=%d" % ldap_backend_extra_port else: - message("LDAP admin DN: %s" % names.ldapmanagerdn) - - message("LDAP admin password: %s" % adminpass) - message(slapdcommand) - if ol_olc == "yes" or ol_mmr_urls is not None: - message("Attention to slapd-Port: <PORT> must be different than 389!") - assert isinstance(ldap_backend_type, str) - assert isinstance(ldapuser, str) - assert isinstance(adminpass, str) - assert isinstance(names.dnsdomain, str) - assert isinstance(names.domain, str) - assert isinstance(serverrole, str) - args = ["--ldap-backend=ldapi", - "--ldap-backend-type=" + ldap_backend_type, - "--password=" + adminpass, - ldapuser, - "--realm=" + names.dnsdomain, - "--domain=" + names.domain, - "--server-role='" + serverrole + "'"] - message("Run provision with: " + " ".join(args)) - - - # if --ol-olc=yes, generate online-configuration in ../private/ldap/slapd.d - if ol_olc == "yes": - if not os.path.isdir(paths.olcdir): - os.makedirs(paths.olcdir, 0770) - paths.olslaptest = str(ol_slaptest) - olc_command = paths.olslaptest + " -f" + paths.slapdconf + " -F" + paths.olcdir + " >/dev/null 2>&1" - os.system(olc_command) - os.remove(paths.slapdconf) - # use line below for debugging during olc-conversion with slaptest, instead of olc_command above - #olc_command = paths.olslaptest + " -f" + paths.slapdconf + " -F" + paths.olcdir" + serverport = "" + + setup_file(setup_path("fedorads.inf"), paths.fedoradsinf, + {"ROOT": root, + "HOSTNAME": hostname, + "DNSDOMAIN": names.dnsdomain, + "LDAPDIR": paths.ldapdir, + "DOMAINDN": names.domaindn, + "LDAPMANAGERDN": names.ldapmanagerdn, + "LDAPMANAGERPASS": ldapadminpass, + "SERVERPORT": serverport}) + + setup_file(setup_path("fedorads-partitions.ldif"), paths.fedoradspartitions, + {"CONFIGDN": names.configdn, + "SCHEMADN": names.schemadn, + "SAMBADN": names.sambadn, + }) + + setup_file(setup_path("fedorads-sasl.ldif"), paths.fedoradssasl, + {"SAMBADN": names.sambadn, + }) + setup_file(setup_path("fedorads-samba.ldif"), paths.fedoradssamba, + {"SAMBADN": names.sambadn, + "LDAPADMINPASS": ldapadminpass + }) + + mapping = "schema-map-fedora-ds-1.0" + backend_schema = "99_ad.ldif" + + # Build a schema file in Fedora DS format + backend_schema_data = schema.ldb.convert_schema_to_openldap("fedora-ds", open(setup_path(mapping), 'r').read()) + assert backend_schema_data is not None + open(os.path.join(paths.ldapdir, backend_schema), 'w').write(backend_schema_data) + + result.credentials.set_bind_dn(names.ldapmanagerdn) + + # Destory the target directory, or else setup-ds.pl will complain + fedora_ds_dir = os.path.join(paths.ldapdir, "slapd-samba4") + shutil.rmtree(fedora_ds_dir, True) + + result.slapd_provision_command = [slapd_path, "-D", fedora_ds_dir, "-i", paths.slapdpid]; + #In the 'provision' command line, stay in the foreground so we can easily kill it + result.slapd_provision_command.append("-d0") + + #the command for the final run is the normal script + result.slapd_command = [os.path.join(paths.ldapdir, "slapd-samba4", "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 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 setup_ds_path is None: + raise ProvisioningError("Warning: 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(setup_ds_path): + message (setup_ds_path) + raise ProvisioningError("Warning: Given Path to slapd does not exist!") + + # Run the Fedora DS setup utility + retcode = subprocess.call([setup_ds_path, "--silent", "--file", paths.fedoradsinf], close_fds=True, shell=False) + if retcode != 0: + raise ProvisioningError("setup-ds failed") + + # Load samba-admin + retcode = subprocess.call([ + os.path.join(paths.ldapdir, "slapd-samba4", "ldif2db"), "-s", names.sambadn, "-i", paths.fedoradssamba], + close_fds=True, shell=False) + if retcode != 0: + raise("ldib2db failed") def create_phpldapadmin_config(path, setup_path, ldapi_uri): """Create a PHP LDAP admin configuration file. @@ -1542,7 +1958,8 @@ def create_phpldapadmin_config(path, setup_path, ldapi_uri): def create_zone_file(path, setup_path, dnsdomain, domaindn, - hostip, hostip6, hostname, dnspass, realm, domainguid, hostguid): + hostip, hostip6, hostname, dnspass, realm, domainguid, + ntdsguid): """Write out a DNS zone file, from the info in the current database. :param path: Path of the new zone file. @@ -1555,7 +1972,7 @@ def create_zone_file(path, setup_path, dnsdomain, domaindn, :param dnspass: Password for DNS :param realm: Realm name :param domainguid: GUID of the domain. - :param hostguid: GUID of the host. + :param ntdsguid: GUID of the hosts nTDSDSA record. """ assert isinstance(domainguid, str) @@ -1583,7 +2000,7 @@ def create_zone_file(path, setup_path, dnsdomain, domaindn, "DOMAINGUID": domainguid, "DATESTRING": time.strftime("%Y%m%d%H"), "DEFAULTSITE": DEFAULTSITE, - "HOSTGUID": hostguid, + "NTDSGUID": ntdsguid, "HOSTIP6_BASE_LINE": hostip6_base_line, "HOSTIP6_HOST_LINE": hostip6_host_line, }) @@ -1648,35 +2065,3 @@ def create_krb5_conf(path, setup_path, dnsdomain, hostname, realm): }) -def load_schema(setup_path, samdb, schemadn, netbiosname, configdn, sitename, - serverdn, servername): - """Load schema for the SamDB. - - :param samdb: Load a schema into a SamDB. - :param setup_path: Setup path function. - :param schemadn: DN of the schema - :param netbiosname: NetBIOS name of the host. - :param configdn: DN of the configuration - :param serverdn: DN of the server - :param servername: Host name of the server - """ - schema_data = open(setup_path("schema.ldif"), 'r').read() - schema_data += open(setup_path("schema_samba4.ldif"), 'r').read() - schema_data = substitute_var(schema_data, {"SCHEMADN": schemadn}) - check_all_substituted(schema_data) - prefixmap = open(setup_path("prefixMap.txt"), 'r').read() - prefixmap = b64encode(prefixmap) - - head_data = open(setup_path("provision_schema_basedn_modify.ldif"), 'r').read() - head_data = substitute_var(head_data, { - "SCHEMADN": schemadn, - "NETBIOSNAME": netbiosname, - "CONFIGDN": configdn, - "DEFAULTSITE": sitename, - "PREFIXMAP_B64": prefixmap, - "SERVERDN": serverdn, - "SERVERNAME": servername, - }) - check_all_substituted(head_data) - samdb.attach_schema_from_ldif(head_data, schema_data) - diff --git a/source4/scripting/python/samba/samba3.py b/source4/scripting/python/samba/samba3.py index c8ddbc8864..179efa2700 100644 --- a/source4/scripting/python/samba/samba3.py +++ b/source4/scripting/python/samba/samba3.py @@ -502,7 +502,7 @@ TDBSAM_USER_PREFIX = "USER_" class LdapSam(object): """Samba 3 LDAP passdb backend reader.""" def __init__(self, url): - self.ldap_url = ldap_url + self.ldap_url = url class TdbSam(TdbDatabase): @@ -692,7 +692,7 @@ class ParamFile(object): (k, v) = l.split("=", 1) self._sections[section][self._sanitize_name(k)] = v else: - raise Error("Unable to parser line %d: %r" % (i+1,l)) + raise Exception("Unable to parser line %d: %r" % (i+1,l)) def get(self, param, section=None): """Return the value of a parameter. diff --git a/source4/scripting/python/samba/samdb.py b/source4/scripting/python/samba/samdb.py index b92a91e2ef..39cf1d6c40 100644 --- a/source4/scripting/python/samba/samdb.py +++ b/source4/scripting/python/samba/samdb.py @@ -2,6 +2,7 @@ # Unix SMB/CIFS implementation. # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2008 +# Copyright (C) Matthias Dieter Wallnoefer 2009 # # Based on the original in EJS: # Copyright (C) Andrew Tridgell <tridge@samba.org> 2005 @@ -35,54 +36,46 @@ __docformat__ = "restructuredText" class SamDB(samba.Ldb): """The SAM database.""" - def __init__(self, url=None, session_info=None, credentials=None, - modules_dir=None, lp=None): - """Open the Sam Database. - - :param url: URL of the database. + def __init__(self, url=None, lp=None, modules_dir=None, session_info=None, + credentials=None, flags=0, options=None): + """Opens the SAM Database + For parameter meanings see the super class (samba.Ldb) """ + self.lp = lp - super(SamDB, self).__init__(session_info=session_info, credentials=credentials, - modules_dir=modules_dir, lp=lp) + if url is None: + url = lp.get("sam database") + + super(SamDB, self).__init__(url=url, lp=lp, modules_dir=modules_dir, + session_info=session_info, credentials=credentials, flags=flags, + options=options) + glue.dsdb_set_global_schema(self) - if url: - self.connect(url) - else: - self.connect(lp.get("sam database")) - - def connect(self, url): - super(SamDB, self).connect(self.lp.private_path(url)) - - def add_foreign(self, domaindn, sid, desc): - """Add a foreign security principle.""" - add = """ -dn: CN=%s,CN=ForeignSecurityPrincipals,%s -objectClass: top -objectClass: foreignSecurityPrincipal -description: %s -""" % (sid, domaindn, desc) - # deliberately ignore errors from this, as the records may - # already exist - for msg in self.parse_ldif(add): - self.add(msg[1]) - - def add_stock_foreign_sids(self): - domaindn = self.domain_dn() - self.add_foreign(domaindn, "S-1-5-7", "Anonymous") - self.add_foreign(domaindn, "S-1-1-0", "World") - self.add_foreign(domaindn, "S-1-5-2", "Network") - self.add_foreign(domaindn, "S-1-5-18", "System") - self.add_foreign(domaindn, "S-1-5-11", "Authenticated Users") - - def enable_account(self, user_dn): - """Enable an account. + + def connect(self, url=None, flags=0, options=None): + super(SamDB, self).connect(url=self.lp.private_path(url), flags=flags, + options=options) + + def domain_dn(self): + # find the DNs for the domain + res = self.search(base="", + scope=ldb.SCOPE_BASE, + expression="(defaultNamingContext=*)", + attrs=["defaultNamingContext"]) + assert(len(res) == 1 and res[0]["defaultNamingContext"] is not None) + return res[0]["defaultNamingContext"][0] + + def enable_account(self, filter): + """Enables an account - :param user_dn: Dn of the account to enable. + :param filter: LDAP filter to find the user (eg samccountname=name) """ - res = self.search(user_dn, ldb.SCOPE_BASE, None, ["userAccountControl"]) - assert len(res) == 1 - userAccountControl = res[0]["userAccountControl"][0] - userAccountControl = int(userAccountControl) + res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE, + expression=filter, attrs=["userAccountControl"]) + assert(len(res) == 1) + user_dn = res[0].dn + + userAccountControl = int(res[0]["userAccountControl"][0]) if (userAccountControl & 0x2): userAccountControl = userAccountControl & ~0x2 # remove disabled bit if (userAccountControl & 0x20): @@ -95,39 +88,51 @@ replace: userAccountControl userAccountControl: %u """ % (user_dn, userAccountControl) self.modify_ldif(mod) + + def force_password_change_at_next_login(self, filter): + """Forces a password change at next login + + :param filter: LDAP filter to find the user (eg samccountname=name) + """ + res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE, + expression=filter, attrs=[]) + assert(len(res) == 1) + user_dn = res[0].dn - def domain_dn(self): - # find the DNs for the domain and the domain users group - res = self.search("", scope=ldb.SCOPE_BASE, - expression="(defaultNamingContext=*)", - attrs=["defaultNamingContext"]) - assert(len(res) == 1 and res[0]["defaultNamingContext"] is not None) - return res[0]["defaultNamingContext"][0] + mod = """ +dn: %s +changetype: modify +replace: pwdLastSet +pwdLastSet: 0 +""" % (user_dn) + self.modify_ldif(mod) + + def newuser(self, username, unixname, password, force_password_change_at_next_login_req=False): + """Adds a new user - def newuser(self, username, unixname, password): - """add a new user record. + Note: This call adds also the ID mapping for winbind; therefore it works + *only* on SAMBA 4. - :param username: Name of the new user. - :param unixname: Name of the unix user to map to. + :param username: Name of the new user + :param unixname: Name of the unix user to map to :param password: Password for the new user + :param force_password_change_at_next_login_req: Force password change """ - # connect to the sam self.transaction_start() try: - domain_dn = self.domain_dn() - assert(domain_dn is not None) - user_dn = "CN=%s,CN=Users,%s" % (username, domain_dn) - - # - # the new user record. note the reliance on the samdb module to - # fill in a sid, guid etc - # - # now the real work + user_dn = "CN=%s,CN=Users,%s" % (username, self.domain_dn()) + + # The new user record. Note the reliance on the SAMLDB module which + # fills in the default informations self.add({"dn": user_dn, "sAMAccountName": username, - "userPassword": password, "objectClass": "user"}) + # Sets the password for it + self.setpassword("(dn=" + user_dn + ")", password, + force_password_change_at_next_login_req) + + # Gets the user SID (for the account mapping setup) res = self.search(user_dn, scope=ldb.SCOPE_BASE, expression="objectclass=*", attrs=["objectSid"]) @@ -138,40 +143,32 @@ userAccountControl: %u idmap = IDmapDB(lp=self.lp) user = pwd.getpwnam(unixname) + # setup ID mapping for this UID - idmap.setup_name_mapping(user_sid, idmap.TYPE_UID, user[2]) except KeyError: pass - - # modify the userAccountControl to remove the disabled bit - self.enable_account(user_dn) except: self.transaction_cancel() raise self.transaction_commit() - def setpassword(self, filter, password): - """Set a password on a user record + def setpassword(self, filter, password, force_password_change_at_next_login_req=False): + """Sets the password for a user + Note: This call uses the "userPassword" attribute to set the password. + This works correctly on SAMBA 4 and on Windows DCs with + "2003 Native" or higer domain function level. + :param filter: LDAP filter to find the user (eg samccountname=name) :param password: Password for the user + :param force_password_change_at_next_login_req: Force password change """ - # connect to the sam self.transaction_start() try: - # find the DNs for the domain - res = self.search("", scope=ldb.SCOPE_BASE, - expression="(defaultNamingContext=*)", - attrs=["defaultNamingContext"]) - assert(len(res) == 1 and res[0]["defaultNamingContext"] is not None) - domain_dn = res[0]["defaultNamingContext"][0] - assert(domain_dn is not None) - - res = self.search(domain_dn, scope=ldb.SCOPE_SUBTREE, - expression=filter, - attrs=[]) + res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE, + expression=filter, attrs=[]) assert(len(res) == 1) user_dn = res[0].dn @@ -184,62 +181,53 @@ userPassword:: %s self.modify_ldif(setpw) + if force_password_change_at_next_login_req: + self.force_password_change_at_next_login( + "(dn=" + str(user_dn) + ")") + # modify the userAccountControl to remove the disabled bit - self.enable_account(user_dn) + self.enable_account(filter) except: self.transaction_cancel() raise self.transaction_commit() - def set_domain_sid(self, sid): - """Change the domain SID used by this SamDB. - - :param sid: The new domain sid to use. - """ - glue.samdb_set_domain_sid(self, sid) - - def attach_schema_from_ldif(self, pf, df): - glue.dsdb_attach_schema_from_ldif_file(self, pf, df) - - def set_invocation_id(self, invocation_id): - """Set the invocation id for this SamDB handle. - - :param invocation_id: GUID of the invocation id. - """ - glue.dsdb_set_ntds_invocation_id(self, invocation_id) - - def setexpiry(self, user, expiry_seconds, noexpiry): - """Set the password expiry for a user + def setexpiry(self, filter, expiry_seconds, no_expiry_req=False): + """Sets the account expiry for a user + :param filter: LDAP filter to find the user (eg samccountname=name) :param expiry_seconds: expiry time from now in seconds - :param noexpiry: if set, then don't expire password + :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=("(samAccountName=%s)" % user), - attrs=["userAccountControl", "accountExpires"]) - assert len(res) == 1 + expression=filter, + attrs=["userAccountControl", "accountExpires"]) + assert(len(res) == 1) + user_dn = res[0].dn + userAccountControl = int(res[0]["userAccountControl"][0]) accountExpires = int(res[0]["accountExpires"][0]) - if noexpiry: + if no_expiry_req: userAccountControl = userAccountControl | 0x10000 accountExpires = 0 else: userAccountControl = userAccountControl & ~0x10000 accountExpires = glue.unix2nttime(expiry_seconds + int(time.time())) - mod = """ + setexp = """ dn: %s changetype: modify replace: userAccountControl userAccountControl: %u replace: accountExpires accountExpires: %u -""" % (res[0].dn, userAccountControl, accountExpires) - # now change the database - self.modify_ldif(mod) +""" % (user_dn, userAccountControl, accountExpires) + + self.modify_ldif(setexp) except: self.transaction_cancel() raise self.transaction_commit(); + diff --git a/source4/scripting/python/samba/shares.py b/source4/scripting/python/samba/shares.py new file mode 100644 index 0000000000..89a312e855 --- /dev/null +++ b/source4/scripting/python/samba/shares.py @@ -0,0 +1,61 @@ +#!/usr/bin/python + +# 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/>. +# + +"""Share management.""" + + +# 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: + 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/source4/scripting/python/samba/tests/__init__.py b/source4/scripting/python/samba/tests/__init__.py index b342b93c49..ae7a707e35 100644 --- a/source4/scripting/python/samba/tests/__init__.py +++ b/source4/scripting/python/samba/tests/__init__.py @@ -26,6 +26,7 @@ import tempfile import unittest class LdbTestCase(unittest.TestCase): + """Trivial test case for running tests against a LDB.""" def setUp(self): self.filename = os.tempnam() @@ -41,6 +42,7 @@ class LdbTestCase(unittest.TestCase): class TestCaseInTempDir(unittest.TestCase): + def setUp(self): super(TestCaseInTempDir, self).setUp() self.tempdir = tempfile.mkdtemp() @@ -52,6 +54,7 @@ class TestCaseInTempDir(unittest.TestCase): class SubstituteVarTestCase(unittest.TestCase): + def test_empty(self): self.assertEquals("", samba.substitute_var("", {})) @@ -75,6 +78,7 @@ class SubstituteVarTestCase(unittest.TestCase): class LdbExtensionTests(TestCaseInTempDir): + def test_searchone(self): path = self.tempdir + "/searchone.ldb" l = samba.Ldb(path) @@ -90,9 +94,22 @@ cmdline_loadparm = None cmdline_credentials = None class RpcInterfaceTestCase(unittest.TestCase): + def get_loadparm(self): assert cmdline_loadparm is not None return cmdline_loadparm def get_credentials(self): return cmdline_credentials + + +class ValidNetbiosNameTests(unittest.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")) diff --git a/source4/scripting/python/samba/tests/dcerpc/__init__.py b/source4/scripting/python/samba/tests/dcerpc/__init__.py index e69de29bb2..c64c9dc9f6 100644 --- a/source4/scripting/python/samba/tests/dcerpc/__init__.py +++ b/source4/scripting/python/samba/tests/dcerpc/__init__.py @@ -0,0 +1,20 @@ +#!/usr/bin/python +# -*- 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/>. +# + diff --git a/source4/scripting/python/samba/tests/dcerpc/bare.py b/source4/scripting/python/samba/tests/dcerpc/bare.py index cd939b8098..6cadad89f1 100644 --- a/source4/scripting/python/samba/tests/dcerpc/bare.py +++ b/source4/scripting/python/samba/tests/dcerpc/bare.py @@ -24,6 +24,7 @@ from samba.tests import cmdline_loadparm class BareTestCase(TestCase): + def test_bare(self): # Connect to the echo pipe x = ClientConnection("ncalrpc:localhost[DEFAULT]", diff --git a/source4/scripting/python/samba/tests/dcerpc/misc.py b/source4/scripting/python/samba/tests/dcerpc/misc.py new file mode 100644 index 0000000000..ab76efc536 --- /dev/null +++ b/source4/scripting/python/samba/tests/dcerpc/misc.py @@ -0,0 +1,62 @@ +#!/usr/bin/python + +# 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/>. +# + +import unittest +from samba.dcerpc import misc + +text1 = "76f53846-a7c2-476a-ae2c-20e2b80d7b34" +text2 = "344edffa-330a-4b39-b96e-2c34da52e8b1" + +class GUIDTests(unittest.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(unittest.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/source4/scripting/python/samba/tests/dcerpc/registry.py b/source4/scripting/python/samba/tests/dcerpc/registry.py index 526b2340cc..f1cd83790d 100644 --- a/source4/scripting/python/samba/tests/dcerpc/registry.py +++ b/source4/scripting/python/samba/tests/dcerpc/registry.py @@ -18,11 +18,11 @@ # from samba.dcerpc import winreg -import unittest from samba.tests import RpcInterfaceTestCase class WinregTests(RpcInterfaceTestCase): + def setUp(self): self.conn = winreg.winreg("ncalrpc:", self.get_loadparm(), self.get_credentials()) diff --git a/source4/scripting/python/samba/tests/dcerpc/rpcecho.py b/source4/scripting/python/samba/tests/dcerpc/rpcecho.py index 62268005c2..72eb87d750 100644 --- a/source4/scripting/python/samba/tests/dcerpc/rpcecho.py +++ b/source4/scripting/python/samba/tests/dcerpc/rpcecho.py @@ -24,6 +24,7 @@ from samba.tests import RpcInterfaceTestCase class RpcEchoTests(RpcInterfaceTestCase): + def setUp(self): self.conn = echo.rpcecho("ncalrpc:", self.get_loadparm()) @@ -49,7 +50,7 @@ class RpcEchoTests(RpcInterfaceTestCase): surrounding_struct.x = 4 surrounding_struct.surrounding = [1,2,3,4] y = self.conn.TestSurrounding(surrounding_struct) - self.assertEquals(4 * [0], y.surrounding) + self.assertEquals(8 * [0], y.surrounding) def test_manual_request(self): self.assertEquals("\x01\x00\x00\x00", self.conn.request(0, chr(0) * 4)) @@ -59,6 +60,7 @@ class RpcEchoTests(RpcInterfaceTestCase): class NdrEchoTests(unittest.TestCase): + def test_info1_push(self): x = echo.info1() x.v = 42 diff --git a/source4/scripting/python/samba/tests/dcerpc/sam.py b/source4/scripting/python/samba/tests/dcerpc/sam.py index 50e00a3f9e..9532333a00 100644 --- a/source4/scripting/python/samba/tests/dcerpc/sam.py +++ b/source4/scripting/python/samba/tests/dcerpc/sam.py @@ -30,6 +30,7 @@ def toArray((handle, array, num_entries)): class SamrTests(RpcInterfaceTestCase): + def setUp(self): self.conn = samr.samr("ncalrpc:", self.get_loadparm()) diff --git a/source4/scripting/python/samba/tests/dcerpc/unix.py b/source4/scripting/python/samba/tests/dcerpc/unix.py index aa47b71b16..62169ad12c 100644 --- a/source4/scripting/python/samba/tests/dcerpc/unix.py +++ b/source4/scripting/python/samba/tests/dcerpc/unix.py @@ -21,6 +21,7 @@ from samba.dcerpc import unixinfo from samba.tests import RpcInterfaceTestCase class UnixinfoTests(RpcInterfaceTestCase): + def setUp(self): self.conn = unixinfo.unixinfo("ncalrpc:", self.get_loadparm()) diff --git a/source4/scripting/python/samba/tests/provision.py b/source4/scripting/python/samba/tests/provision.py index fdac9d4ea2..f34073504c 100644 --- a/source4/scripting/python/samba/tests/provision.py +++ b/source4/scripting/python/samba/tests/provision.py @@ -18,7 +18,7 @@ # import os -from samba.provision import setup_secretsdb, secretsdb_become_dc, findnss +from samba.provision import setup_secretsdb, findnss import samba.tests from ldb import Dn from samba import param @@ -44,30 +44,6 @@ class ProvisionTestCase(samba.tests.TestCaseInTempDir): del ldb os.unlink(path) - def test_become_dc(self): - path = os.path.join(self.tempdir, "secrets.ldb") - secrets_ldb = setup_secretsdb(path, setup_path, None, None, lp=lp) - try: - secretsdb_become_dc(secrets_ldb, setup_path, domain="EXAMPLE", - realm="example", netbiosname="myhost", - domainsid="S-5-22", keytab_path="keytab.path", - samdb_url="ldap://url/", - dns_keytab_path="dns.keytab", dnspass="bla", - machinepass="machinepass", dnsdomain="example.com") - self.assertEquals(1, - len(secrets_ldb.search("samAccountName=krbtgt,flatname=EXAMPLE,CN=Principals"))) - self.assertEquals("keytab.path", - secrets_ldb.searchone(basedn="flatname=EXAMPLE,CN=primary domains", - expression="(privateKeytab=*)", - attribute="privateKeytab")) - self.assertEquals("S-5-22", - secrets_ldb.searchone(basedn="flatname=EXAMPLE,CN=primary domains", - expression="objectSid=*", attribute="objectSid")) - - finally: - del secrets_ldb - os.unlink(path) - class FindNssTests(unittest.TestCase): """Test findnss() function.""" diff --git a/source4/scripting/python/samba/tests/samdb.py b/source4/scripting/python/samba/tests/samdb.py index d0b95cf542..8c7bb0ae98 100644 --- a/source4/scripting/python/samba/tests/samdb.py +++ b/source4/scripting/python/samba/tests/samdb.py @@ -72,12 +72,11 @@ class SamDBTestCase(TestCaseInTempDir): domaindn=self.domaindn, configdn=configdn, schemadn=schemadn) setup_templatesdb(os.path.join(self.tempdir, "templates.ldb"), - self.setup_path, session_info=session_info, - credentials=creds, lp=self.lp) + self.setup_path, session_info=session_info, lp=self.lp) self.samdb = setup_samdb(path, self.setup_path, session_info, creds, self.lp, names, lambda x: None, domainsid, - "# no aci", domainguid, + domainguid, policyguid, False, "secret", "secret", "secret", invocationid, "secret", "domain controller") @@ -89,9 +88,9 @@ class SamDBTestCase(TestCaseInTempDir): super(SamDBTestCase, self).tearDown() +# disable this test till andrew works it out ... class SamDBTests(SamDBTestCase): """Tests for the SamDB implementation.""" - def test_add_foreign(self): - self.samdb.add_foreign(self.domaindn, "S-1-5-7", "Somedescription") - + print "samdb add_foreign disabled for now" +# def test_add_foreign(self): diff --git a/source4/scripting/python/samba/tests/shares.py b/source4/scripting/python/samba/tests/shares.py new file mode 100644 index 0000000000..9130c36780 --- /dev/null +++ b/source4/scripting/python/samba/tests/shares.py @@ -0,0 +1,74 @@ +#!/usr/bin/python + +# 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/>. +# +from samba.shares import SharesContainer +from unittest 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 __contains__(self, name): + return name in self.data + + 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_nonexistant(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/source4/scripting/python/samba/upgrade.py b/source4/scripting/python/samba/upgrade.py index 0c83604e82..81945525e6 100644 --- a/source4/scripting/python/samba/upgrade.py +++ b/source4/scripting/python/samba/upgrade.py @@ -9,17 +9,16 @@ __docformat__ = "restructuredText" -from provision import findnss, provision, FILL_DRS +from provision import provision, FILL_DRS import grp import ldb import time import pwd -import uuid import registry from samba import Ldb -from samba.samdb import SamDB +from samba.param import LoadParm -def import_sam_policy(samldb, samba3_policy, domaindn): +def import_sam_policy(samldb, policy, dn): """Import a Samba 3 policy database.""" samldb.modify_ldif(""" dn: %s @@ -394,7 +393,7 @@ def upgrade_smbconf(oldconf,mark): kept in the new configuration as "samba3:<name>" """ data = oldconf.data() - newconf = param_init() + newconf = LoadParm() for s in data: for p in data[s]: |