diff options
author | bubulle <bubulle@alioth.debian.org> | 2012-01-26 19:58:37 +0000 |
---|---|---|
committer | bubulle <bubulle@alioth.debian.org> | 2012-01-26 19:58:37 +0000 |
commit | cb25bc5ca98dff7a896f596f9f1586a4739ad8ec (patch) | |
tree | 31bd310956a0c533e3e46cb88aec6e00b5eedf53 /source4/scripting/python | |
parent | 5f021ee1efe415ba8fe4281d0622204a68074ea8 (diff) | |
download | samba-cb25bc5ca98dff7a896f596f9f1586a4739ad8ec.tar.gz |
Load samba-3.6.2 into branches/samba/upstream.upstream/3.6.2
git-svn-id: svn://svn.debian.org/svn/pkg-samba/branches/samba/upstream@3992 fc4039ab-9d04-0410-8cac-899223bdd6b0
Diffstat (limited to 'source4/scripting/python')
93 files changed, 13389 insertions, 4234 deletions
diff --git a/source4/scripting/python/config.mk b/source4/scripting/python/config.mk deleted file mode 100644 index a5e3f25d59..0000000000 --- a/source4/scripting/python/config.mk +++ /dev/null @@ -1,35 +0,0 @@ -[SUBSYSTEM::LIBPYTHON] -PUBLIC_DEPENDENCIES = EXT_LIB_PYTHON -PRIVATE_DEPENDENCIES = PYTALLOC -INIT_FUNCTION_SENTINEL = { NULL, NULL } - -LIBPYTHON_OBJ_FILES = $(addprefix $(pyscriptsrcdir)/, modules.o) - -[SUBSYSTEM::PYTALLOC] -PUBLIC_DEPENDENCIES = EXT_LIB_PYTHON LIBTALLOC - -PYTALLOC_OBJ_FILES = ../lib/talloc/pytalloc.o - -[PYTHON::python_uuid] -PRIVATE_DEPENDENCIES = LIBNDR - -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 pyldb_util pyparam_util - -python_glue_OBJ_FILES = $(pyscriptsrcdir)/pyglue.o - -$(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)/%,%,$(subst ../lib/subunit/python,,$(pyfile))),$(pyfile)))) - -EPYDOC_OPTIONS = --no-private --url http://www.samba.org/ --no-sourcecode - -epydoc:: pythonmods - PYTHONPATH=$(pythonbuilddir):../lib/subunit/python epydoc $(EPYDOC_OPTIONS) samba tdb ldb subunit - -install:: installpython diff --git a/source4/scripting/python/examples/netbios.py b/source4/scripting/python/examples/netbios.py index 3671076a59..a29b09f051 100644 --- a/source4/scripting/python/examples/netbios.py +++ b/source4/scripting/python/examples/netbios.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # Unix SMB/CIFS implementation. # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2008 diff --git a/source4/scripting/python/examples/samr.py b/source4/scripting/python/examples/samr.py index c0e3167a97..9b8e31e434 100755 --- a/source4/scripting/python/examples/samr.py +++ b/source4/scripting/python/examples/samr.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # -*- coding: utf-8 -*- # Unix SMB/CIFS implementation. diff --git a/source4/scripting/python/examples/winreg.py b/source4/scripting/python/examples/winreg.py index 80b48ecfd7..484a69b842 100755 --- a/source4/scripting/python/examples/winreg.py +++ b/source4/scripting/python/examples/winreg.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # # tool to manipulate a remote registry # Copyright Andrew Tridgell 2005 diff --git a/source4/scripting/python/modules.c b/source4/scripting/python/modules.c index e53f4cfaf2..78cdbc0d87 100644 --- a/source4/scripting/python/modules.c +++ b/source4/scripting/python/modules.c @@ -17,54 +17,47 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ +#include <Python.h> #include "includes.h" #include "scripting/python/modules.h" -#include <Python.h> +#include "dynconfig/dynconfig.h" -extern void init_ldb(void); -extern void init_security(void); -extern void init_registry(void); -extern void init_param(void); -extern void init_misc(void); -extern void init_ldb(void); -extern void init_auth(void); -extern void init_credentials(void); -extern void init_tdb(void); -extern void init_dcerpc(void); -extern void init_events(void); -extern void inituuid(void); -extern void init_net(void); -extern void initecho(void); -extern void initdfs(void); -extern void initdrsuapi(void); -extern void initwinreg(void); -extern void initepmapper(void); -extern void initinitshutdown(void); -extern void initmgmt(void); -extern void initnet(void); -extern void initatsvc(void); -extern void initsamr(void); -extern void initlsa(void); -extern void initsvcctl(void); -extern void initwkssvc(void); -extern void initunixinfo(void); -extern void init_libcli_nbt(void); -extern void init_libcli_smb(void); +static bool PySys_PathPrepend(PyObject *list, const char *path) +{ + PyObject *py_path = PyString_FromString(path); + if (py_path == NULL) + return false; -static struct _inittab py_modules[] = { STATIC_LIBPYTHON_MODULES }; + return (PyList_Insert(list, 0, py_path) == 0); +} -void py_load_samba_modules(void) +bool py_update_path(void) { - int i; - for (i = 0; i < ARRAY_SIZE(py_modules); i++) { - PyImport_ExtendInittab(&py_modules[i]); + PyObject *mod_sys, *py_path; + + mod_sys = PyImport_ImportModule("sys"); + if (mod_sys == NULL) { + return false; } -} -void py_update_path(const char *bindir) -{ - char *newpath; - asprintf(&newpath, "%s/python:%s/../scripting/python:%s", bindir, bindir, Py_GetPath()); - PySys_SetPath(newpath); - free(newpath); + py_path = PyObject_GetAttrString(mod_sys, "path"); + if (py_path == NULL) { + return false; + } + + if (!PyList_Check(py_path)) { + return false; + } + + if (!PySys_PathPrepend(py_path, dyn_PYTHONDIR)) { + return false; + } + + if (strcmp(dyn_PYTHONARCHDIR, dyn_PYTHONDIR) != 0) { + if (!PySys_PathPrepend(py_path, dyn_PYTHONARCHDIR)) { + return false; + } + } + + return true; } diff --git a/source4/scripting/python/modules.h b/source4/scripting/python/modules.h index 6b242ee257..e7e97aa1bf 100644 --- a/source4/scripting/python/modules.h +++ b/source4/scripting/python/modules.h @@ -20,9 +20,6 @@ #ifndef __SAMBA_PYTHON_MODULES_H__ #define __SAMBA_PYTHON_MODULES_H__ -void py_load_samba_modules(void); -void py_update_path(const char *bindir); - -#define py_iconv_convenience(mem_ctx) smb_iconv_convenience_init(mem_ctx, "ASCII", PyUnicode_GetDefaultEncoding(), true) +bool py_update_path(void); #endif /* __SAMBA_PYTHON_MODULES_H__ */ diff --git a/source4/scripting/python/pyglue.c b/source4/scripting/python/pyglue.c index 753f2df464..f89785f971 100644 --- a/source4/scripting/python/pyglue.c +++ b/source4/scripting/python/pyglue.c @@ -17,55 +17,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ +#include <Python.h> #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" -#include "lib/ldb-samba/ldif_handlers.h" -#include "librpc/ndr/libndr.h" #include "version.h" -#include <Python.h> -#include "lib/ldb/pyldb.h" -#include "libcli/util/pyerrors.h" -#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 -#endif - -/* FIXME: These should be in a header file somewhere, once we finish moving - * away from SWIG .. */ -#define PyErr_LDB_OR_RAISE(py_ldb, ldb) \ -/* 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))); -} +#include "lib/socket/netif.h" -static PyObject *py_ldb_get_exception(void) -{ - PyObject *mod = PyImport_ImportModule("ldb"); - if (mod == NULL) - return NULL; - - return PyObject_GetAttrString(mod, "LdbError"); -} +void init_glue(void); static PyObject *py_generate_random_str(PyObject *self, PyObject *args) { @@ -81,473 +39,181 @@ static PyObject *py_generate_random_str(PyObject *self, PyObject *args) return ret; } -static PyObject *py_unix2nttime(PyObject *self, PyObject *args) -{ - time_t t; - NTTIME nt; - if (!PyArg_ParseTuple(args, "I", &t)) - return NULL; - - unix_to_nt_time(&nt, t); - - 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; - struct cli_credentials *creds; - struct ldb_context *ldb; - if (!PyArg_ParseTuple(args, "OO", &py_ldb, &py_creds)) - return NULL; - - PyErr_LDB_OR_RAISE(py_ldb, ldb); - - creds = cli_credentials_from_py_object(py_creds); - if (creds == NULL) { - PyErr_SetString(PyExc_TypeError, "Expected credentials object"); - return NULL; - } - - ldb_set_opaque(ldb, "credentials", creds); - - Py_RETURN_NONE; -} - -static PyObject *py_ldb_set_loadparm(PyObject *self, PyObject *args) +static PyObject *py_generate_random_password(PyObject *self, PyObject *args) { - PyObject *py_lp_ctx, *py_ldb; - struct loadparm_context *lp_ctx; - struct ldb_context *ldb; - if (!PyArg_ParseTuple(args, "OO", &py_ldb, &py_lp_ctx)) - return NULL; - - PyErr_LDB_OR_RAISE(py_ldb, ldb); - - lp_ctx = lp_from_py_object(py_lp_ctx); - if (lp_ctx == NULL) { - PyErr_SetString(PyExc_TypeError, "Expected loadparm object"); - return NULL; - } - - ldb_set_opaque(ldb, "loadparm", lp_ctx); - - Py_RETURN_NONE; -} - - -static PyObject *py_ldb_set_session_info(PyObject *self, PyObject *args) -{ - PyObject *py_session_info, *py_ldb; - struct auth_session_info *info; - struct ldb_context *ldb; - if (!PyArg_ParseTuple(args, "OO", &py_ldb, &py_session_info)) - return NULL; - - PyErr_LDB_OR_RAISE(py_ldb, ldb); - /*if (!PyAuthSession_Check(py_session_info)) { - PyErr_SetString(PyExc_TypeError, "Expected session info object"); - return NULL; - }*/ - - info = PyAuthSession_AsSession(py_session_info); - - ldb_set_opaque(ldb, "sessionInfo", info); - - 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; - struct ldb_context *ldb; - struct dom_sid *sid; - bool ret; - - if (!PyArg_ParseTuple(args, "OO", &py_ldb, &py_sid)) - return NULL; - - PyErr_LDB_OR_RAISE(py_ldb, ldb); - - sid = dom_sid_parse_talloc(NULL, PyString_AsString(py_sid)); - - ret = samdb_set_domain_sid(ldb, sid); - if (!ret) { - PyErr_SetString(PyExc_RuntimeError, "set_domain_sid failed"); - return NULL; - } - 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; + int min, max; PyObject *ret; char *retstr; - - if (!PyArg_ParseTuple(args, "O", &py_ldb)) + if (!PyArg_ParseTuple(args, "ii", &min, &max)) return NULL; - - PyErr_LDB_OR_RAISE(py_ldb, ldb); - sid = samdb_domain_sid(ldb); - if (!sid) { - PyErr_SetString(PyExc_RuntimeError, "samdb_domain_sid failed"); + retstr = generate_random_password(NULL, min, max); + if (retstr == NULL) { 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) +static PyObject *py_unix2nttime(PyObject *self, PyObject *args) { - PyObject *py_ldb; - struct ldb_context *ldb; - int ret; - - if (!PyArg_ParseTuple(args, "O", &py_ldb)) + time_t t; + NTTIME nt; + if (!PyArg_ParseTuple(args, "I", &t)) return NULL; - PyErr_LDB_OR_RAISE(py_ldb, ldb); - ret = ldb_register_samba_handlers(ldb); + unix_to_nt_time(&nt, t); - PyErr_LDB_ERROR_IS_ERR_RAISE(py_ldb_get_exception(), ret, ldb); - Py_RETURN_NONE; + return PyLong_FromLongLong((uint64_t)nt); } -static PyObject *py_dsdb_set_ntds_invocation_id(PyObject *self, PyObject *args) +static PyObject *py_nttime2unix(PyObject *self, PyObject *args) { - PyObject *py_ldb, *py_guid; - bool ret; - struct GUID guid; - struct ldb_context *ldb; - if (!PyArg_ParseTuple(args, "OO", &py_ldb, &py_guid)) + time_t t; + NTTIME nt; + if (!PyArg_ParseTuple(args, "K", &nt)) return NULL; - PyErr_LDB_OR_RAISE(py_ldb, ldb); - GUID_from_string(PyString_AsString(py_guid), &guid); + t = nt_time_to_unix(nt); - ret = samdb_set_ntds_invocation_id(ldb, &guid); - if (!ret) { - PyErr_SetString(PyExc_RuntimeError, "set_ntds_invocation_id failed"); - return NULL; - } - Py_RETURN_NONE; + return PyInt_FromLong((uint64_t)t); } -static PyObject *py_dsdb_set_opaque_integer(PyObject *self, PyObject *args) +static PyObject *py_nttime2string(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; + PyObject *ret; + NTTIME nt; TALLOC_CTX *tmp_ctx; - - if (!PyArg_ParseTuple(args, "Osi", &py_ldb, &py_opaque_name, &value)) + const char *string; + if (!PyArg_ParseTuple(args, "K", &nt)) 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); + tmp_ctx = talloc_new(NULL); 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; + PyErr_NoMemory(); + return NULL; } - - *new_val = value; - /* cache the domain_sid in the ldb */ - if (ldb_set_opaque(ldb, opaque_name_talloc, new_val) != LDB_SUCCESS) { - goto failed; - } + string = nt_time_string(tmp_ctx, nt); + ret = PyString_FromString(string); - 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; + return ret; } -static PyObject *py_dsdb_set_global_schema(PyObject *self, PyObject *args) +static PyObject *py_set_debug_level(PyObject *self, PyObject *args) { - PyObject *py_ldb; - struct ldb_context *ldb; - int ret; - if (!PyArg_ParseTuple(args, "O", &py_ldb)) + unsigned level; + if (!PyArg_ParseTuple(args, "I", &level)) return NULL; - - PyErr_LDB_OR_RAISE(py_ldb, ldb); - - ret = dsdb_set_global_schema(ldb); - PyErr_LDB_ERROR_IS_ERR_RAISE(py_ldb_get_exception(), ret, ldb); - + (DEBUGLEVEL) = level; Py_RETURN_NONE; } -static PyObject *py_dsdb_set_schema_from_ldif(PyObject *self, PyObject *args) +static PyObject *py_get_debug_level(PyObject *self) { - WERROR result; - char *pf, *df; - PyObject *py_ldb; - struct ldb_context *ldb; - - if (!PyArg_ParseTuple(args, "Oss", &py_ldb, &pf, &df)) - return NULL; - - PyErr_LDB_OR_RAISE(py_ldb, ldb); - - result = dsdb_set_schema_from_ldif(ldb, pf, df); - PyErr_WERROR_IS_ERR_RAISE(result); - - Py_RETURN_NONE; + return PyInt_FromLong(DEBUGLEVEL); } -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; -} +/* + return the list of interface IPs we have configured + takes an loadparm context, returns a list of IPs in string form -static PyObject *py_dsdb_write_prefixes_from_schema_to_ldb(PyObject *self, PyObject *args) + Does not return addresses on 127.0.0.0/8 + */ +static PyObject *py_interface_ips(PyObject *self, PyObject *args) { - PyObject *py_ldb; - struct ldb_context *ldb; - WERROR result; - struct dsdb_schema *schema; + PyObject *pylist; + int count; + TALLOC_CTX *tmp_ctx; + PyObject *py_lp_ctx; + struct loadparm_context *lp_ctx; + struct interface *ifaces; + int i, ifcount; + int all_interfaces; - if (!PyArg_ParseTuple(args, "O", &py_ldb)) + if (!PyArg_ParseTuple(args, "Oi", &py_lp_ctx, &all_interfaces)) 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"); + tmp_ctx = talloc_new(NULL); + if (tmp_ctx == NULL) { + PyErr_NoMemory(); 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"); + lp_ctx = lpcfg_from_py_object(tmp_ctx, py_lp_ctx); + if (lp_ctx == NULL) { + talloc_free(tmp_ctx); return NULL; } - ret = dsdb_reference_schema(ldb, schema, true); - PyErr_LDB_ERROR_IS_ERR_RAISE(py_ldb_get_exception(), ret, ldb); + load_interfaces(tmp_ctx, lpcfg_interfaces(lp_ctx), &ifaces); - Py_RETURN_NONE; -} + count = iface_count(ifaces); -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; + /* first count how many are not loopback addresses */ + for (ifcount = i = 0; i<count; i++) { + const char *ip = iface_n_ip(ifaces, i); + if (!(!all_interfaces && iface_same_net(ip, "127.0.0.1", "255.0.0.0"))) { + ifcount++; + } } - return PyInt_FromLong(rid); + pylist = PyList_New(ifcount); + for (ifcount = i = 0; i<count; i++) { + const char *ip = iface_n_ip(ifaces, i); + if (!(!all_interfaces && iface_same_net(ip, "127.0.0.1", "255.0.0.0"))) { + PyList_SetItem(pylist, ifcount, PyString_FromString(ip)); + ifcount++; + } + } + talloc_free(tmp_ctx); + return pylist; } static PyMethodDef py_misc_methods[] = { { "generate_random_str", (PyCFunction)py_generate_random_str, METH_VARARGS, - "random_password(len) -> string\n" - "Generate random password with specified length." }, + "generate_random_str(len) -> string\n" + "Generate random string with specified length." }, + { "generate_random_password", (PyCFunction)py_generate_random_password, + METH_VARARGS, "generate_random_password(min, max) -> string\n" + "Generate random password with a length >= min and <= max." }, { "unix2nttime", (PyCFunction)py_unix2nttime, METH_VARARGS, "unix2nttime(timestamp) -> nttime" }, - { "ldb_set_credentials", (PyCFunction)py_ldb_set_credentials, METH_VARARGS, - "ldb_set_credentials(ldb, credentials) -> None\n" - "Set credentials to use when connecting." }, - { "ldb_set_session_info", (PyCFunction)py_ldb_set_session_info, METH_VARARGS, - "ldb_set_session_info(ldb, session_info)\n" - "Set session info to use when connecting." }, - { "ldb_set_loadparm", (PyCFunction)py_ldb_set_loadparm, METH_VARARGS, - "ldb_set_loadparm(ldb, session_info)\n" - "Set loadparm context to use when connecting." }, - { "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_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 }, + { "nttime2unix", (PyCFunction)py_nttime2unix, METH_VARARGS, + "nttime2unix(nttime) -> timestamp" }, + { "nttime2string", (PyCFunction)py_nttime2string, METH_VARARGS, + "nttime2string(nttime) -> string" }, { "set_debug_level", (PyCFunction)py_set_debug_level, METH_VARARGS, "set debug level" }, + { "get_debug_level", (PyCFunction)py_get_debug_level, METH_NOARGS, + "get debug level" }, + { "interface_ips", (PyCFunction)py_interface_ips, METH_VARARGS, + "get interface IP address list"}, { NULL } }; -void initglue(void) +void init_glue(void) { PyObject *m; - m = Py_InitModule3("glue", py_misc_methods, + debug_setup_talloc_log(); + + m = Py_InitModule3("_glue", py_misc_methods, "Python bindings for miscellaneous Samba functions."); if (m == NULL) 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)); + PyModule_AddObject(m, "version", + PyString_FromString(SAMBA_VERSION_STRING)); + + /* one of the most annoying things about python scripts is + that they don't die when you hit control-C. This fixes that + sillyness. As we do all database operations using + transactions, this is also safe. + */ + signal(SIGINT, SIG_DFL); } diff --git a/source4/scripting/python/samba/__init__.py b/source4/scripting/python/samba/__init__.py index 82df4960cf..2a54f47d2b 100644 --- a/source4/scripting/python/samba/__init__.py +++ b/source4/scripting/python/samba/__init__.py @@ -1,21 +1,21 @@ -#!/usr/bin/python +#!/usr/bin/env python # Unix SMB/CIFS implementation. # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2008 -# +# # Based on the original in EJS: # Copyright (C) Andrew Tridgell <tridge@samba.org> 2005 -# +# # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. -# +# # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # @@ -25,33 +25,39 @@ __docformat__ = "restructuredText" import os +import sys + +def source_tree_topdir(): + '''return the top level directory (the one containing the source4 directory)''' + paths = [ "../../..", "../../../.." ] + for p in paths: + topdir = os.path.normpath(os.path.join(os.path.dirname(__file__), p)) + if os.path.exists(os.path.join(topdir, 'source4')): + return topdir + raise RuntimeError("unable to find top level source directory") + +def in_source_tree(): + '''return True if we are running from within the samba source tree''' + try: + topdir = source_tree_topdir() + except RuntimeError: + return False + return True -def _in_source_tree(): - """Check whether the script is being run from the source dir. """ - return os.path.exists("%s/../../../selftest/skip" % os.path.dirname(__file__)) - - -# When running, in-tree, make sure bin/python is in the PYTHONPATH -if _in_source_tree(): - import sys - srcdir = "%s/../../.." % os.path.dirname(__file__) - sys.path.append("%s/bin/python" % srcdir) - default_ldb_modules_dir = "%s/bin/modules/ldb" % srcdir -else: - default_ldb_modules_dir = None import ldb -import glue +from samba._ldb import Ldb as _Ldb -class Ldb(ldb.Ldb): - """Simple Samba-specific LDB subclass that takes care +class Ldb(_Ldb): + """Simple Samba-specific LDB subclass that takes care of setting up the modules dir, credentials pointers, etc. - - Please note that this is intended to be for all Samba LDB files, - not necessarily the Sam database. For Sam-specific helper + + Please note that this is intended to be for all Samba LDB files, + not necessarily the Sam database. For Sam-specific helper functions see samdb.py. """ + def __init__(self, url=None, lp=None, modules_dir=None, session_info=None, credentials=None, flags=0, options=None): """Opens a Samba Ldb file. @@ -65,14 +71,12 @@ class Ldb(ldb.Ldb): :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 + modules-dir is used by default and that credentials and session_info can be passed through (required by some modules). """ if modules_dir is not None: self.set_modules_dir(modules_dir) - elif default_ldb_modules_dir is not None: - self.set_modules_dir(default_ldb_modules_dir) elif lp is not None: self.set_modules_dir(os.path.join(lp.get("modules dir"), "ldb")) @@ -88,37 +92,30 @@ class Ldb(ldb.Ldb): # 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) + self.register_samba_handlers() # TODO set debug - def msg(l,text): + def msg(l, text): print text #self.set_debug(msg) - glue.ldb_set_utf8_casefold(self) + self.set_utf8_casefold() # Allow admins to force non-sync ldb for all databases if lp is not None: nosync_p = lp.get("nosync", "ldb") - if nosync_p is not None and nosync_p == true: - flags |= FLG_NOSYNC + if nosync_p is not None and nosync_p == True: + flags |= ldb.FLG_NOSYNC + + self.set_create_perms(0600) if url is not None: self.connect(url, flags, options) - def set_credentials(self, credentials): - glue.ldb_set_credentials(self, credentials) - - def set_session_info(self, session_info): - glue.ldb_set_session_info(self, session_info) - - def set_loadparm(self, lp_ctx): - glue.ldb_set_loadparm(self, lp_ctx) - - def searchone(self, attribute, basedn=None, expression=None, + def searchone(self, attribute, basedn=None, expression=None, scope=ldb.SCOPE_BASE): """Search for one attribute as a string. - + :param basedn: BaseDN for the search. :param attribute: Name of the attribute :param expression: Optional search expression. @@ -133,25 +130,36 @@ class Ldb(ldb.Ldb): 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.""" + """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 + except ldb.LdbError, (errno, _): + if errno == ldb.ERR_NO_SUCH_OBJECT: + # Ignore no such object errors + return + else: + raise try: for msg in res: - self.delete(msg.dn) - except ldb.LdbError, (ldb.ERR_NO_SUCH_OBJECT, _): - # Ignore no such object errors - return + self.delete(msg.dn, ["relax:0"]) + except ldb.LdbError, (errno, _): + if errno != ldb.ERR_NO_SUCH_OBJECT: + # Ignore no such object errors + raise def erase_except_schema_controlled(self): - """Erase this ldb, removing all records, except those that are controlled by Samba4's schema.""" + """Erase this ldb. + + :note: Removes all records, except those that are controlled by + Samba4's schema. + """ basedn = "" @@ -159,73 +167,42 @@ class Ldb(ldb.Ldb): 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"]): + for msg in self.search(basedn, ldb.SCOPE_SUBTREE, + "(&(|(objectclass=*)(distinguishedName=*))(!(distinguishedName=@BASEINFO)))", + [], controls=["show_deleted:0", "show_recycled:0"]): try: - self.delete(msg.dn) - 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"]) + self.delete(msg.dn, ["relax:0"]) + except ldb.LdbError, (errno, _): + if errno != ldb.ERR_NO_SUCH_OBJECT: + # Ignore no such object errors + raise + + res = self.search(basedn, ldb.SCOPE_SUBTREE, + "(&(|(objectclass=*)(distinguishedName=*))(!(distinguishedName=@BASEINFO)))", [], controls=["show_deleted:0", "show_recycled:0"]) assert len(res) == 0 # delete the specials - for attr in ["@SUBCLASSES", "@MODULES", + 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 + self.delete(attr, ["relax:0"]) + except ldb.LdbError, (errno, _): + if errno != ldb.ERR_NO_SUCH_OBJECT: + # Ignore missing dn errors + raise def erase(self): """Erase this ldb, removing all records.""" - self.erase_except_schema_controlled() # delete the specials for attr in ["@INDEXLIST", "@ATTRIBUTES"]: try: - self.delete(attr) - except ldb.LdbError, (ldb.ERR_NO_SUCH_OBJECT, _): - # Ignore missing dn errors - pass - - def erase_partitions(self): - """Erase an ldb, removing all records.""" - - def erase_recursive(self, dn): - try: - 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) - - try: - self.delete(dn) - except ldb.LdbError, (ldb.ERR_NO_SUCH_OBJECT, _): - # Ignore no such object errors - pass - - 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"]: - # 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) + self.delete(attr, ["relax:0"]) + except ldb.LdbError, (errno, _): + if errno != ldb.ERR_NO_SUCH_OBJECT: + # Ignore missing dn errors + raise def load_ldif_file_add(self, ldif_path): """Load a LDIF file. @@ -234,68 +211,31 @@ class Ldb(ldb.Ldb): """ self.add_ldif(open(ldif_path, 'r').read()) - def add_ldif(self, ldif): + def add_ldif(self, ldif, controls=None): """Add data based on a LDIF string. :param ldif: LDIF text. """ for changetype, msg in self.parse_ldif(ldif): assert changetype == ldb.CHANGETYPE_NONE - self.add(msg) + self.add(msg, controls) - def modify_ldif(self, ldif): + def modify_ldif(self, ldif, controls=None): """Modify database based on a LDIF string. :param ldif: LDIF text. """ for changetype, msg in self.parse_ldif(ldif): - 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) + if changetype == ldb.CHANGETYPE_ADD: + self.add(msg, controls) + else: + self.modify(msg, controls) def substitute_var(text, values): - """substitute strings of the form ${NAME} in str, replacing - with substitutions from subobj. - + """Substitute strings of the form ${NAME} in str, replacing + with substitutions from values. + :param text: Text in which to subsitute. :param values: Dictionary with keys and values. """ @@ -309,18 +249,51 @@ def substitute_var(text, values): def check_all_substituted(text): - """Make sure that all substitution variables in a string have been replaced. + """Check that all substitution variables in a string have been replaced. + If not, raise an exception. - + :param text: The text to search for substitution variables """ if not "${" in text: return - + var_start = text.find("${") var_end = text.find("}", var_start) - - raise Exception("Not all variables substituted: %s" % text[var_start:var_end+1]) + + raise Exception("Not all variables substituted: %s" % + text[var_start:var_end+1]) + + +def read_and_sub_file(file_name, subst_vars): + """Read a file and sub in variables found in it + + :param file_name: File to be read (typically from setup directory) + param subst_vars: Optional variables to subsitute in the file. + """ + data = open(file_name, 'r').read() + if subst_vars is not None: + data = substitute_var(data, subst_vars) + check_all_substituted(data) + return data + + +def setup_file(template, fname, subst_vars=None): + """Setup a file in the private dir. + + :param template: Path of the template file. + :param fname: Path of the file to create. + :param subst_vars: Substitution variables. + """ + if os.path.exists(fname): + os.unlink(fname) + + data = read_and_sub_file(template, subst_vars) + f = open(fname, 'w') + try: + f.write(data) + finally: + f.close() def valid_netbios_name(name): @@ -334,56 +307,44 @@ def valid_netbios_name(name): return True -def dom_sid_to_rid(sid_str): - """Converts a domain SID to the relative RID. +def import_bundled_package(modulename, location): + """Import the bundled version of a package. - :param sid_str: The domain SID formatted as string + :note: This should only be called if the system version of the package + is not adequate. + + :param modulename: Module name to import + :param location: Location to add to sys.path (can be relative to + ${srcdir}/lib) """ + if in_source_tree(): + sys.path.insert(0, os.path.join(source_tree_topdir(), "lib", location)) + sys.modules[modulename] = __import__(modulename) + else: + sys.modules[modulename] = __import__( + "samba.external.%s" % modulename, fromlist=["samba.external"]) + - 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 +def ensure_external_module(modulename, location): + """Add a location to sys.path if an external dependency can't be found. + :param modulename: Module name to import + :param location: Location to add to sys.path (can be relative to + ${srcdir}/lib) + """ + try: + __import__(modulename) + except ImportError: + import_bundled_package(modulename, location) + + +from samba import _glue +version = _glue.version +interface_ips = _glue.interface_ips +set_debug_level = _glue.set_debug_level +get_debug_level = _glue.get_debug_level +unix2nttime = _glue.unix2nttime +nttime2string = _glue.nttime2string +nttime2unix = _glue.nttime2unix +unix2nttime = _glue.unix2nttime +generate_random_password = _glue.generate_random_password diff --git a/source4/scripting/python/samba/drs_utils.py b/source4/scripting/python/samba/drs_utils.py new file mode 100644 index 0000000000..77f415ed17 --- /dev/null +++ b/source4/scripting/python/samba/drs_utils.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python +# +# DRS utility code +# +# Copyright Andrew Tridgell 2010 +# Copyright Andrew Bartlett 2010 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +from samba.dcerpc import drsuapi, misc +from samba.net import Net +import samba, ldb + + +def drs_DsBind(drs): + '''make a DsBind call, returning the binding handle''' + bind_info = drsuapi.DsBindInfoCtr() + bind_info.length = 28 + bind_info.info = drsuapi.DsBindInfo28() + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_BASE + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_ASYNC_REPLICATION + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_REMOVEAPI + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_MOVEREQ_V2 + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_GETCHG_COMPRESS + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_DCINFO_V1 + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_RESTORE_USN_OPTIMIZATION + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_KCC_EXECUTE + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_ADDENTRY_V2 + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_LINKED_VALUE_REPLICATION + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_DCINFO_V2 + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_INSTANCE_TYPE_NOT_REQ_ON_MOD + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_CRYPTO_BIND + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_GET_REPL_INFO + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_STRONG_ENCRYPTION + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_DCINFO_V01 + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_TRANSITIVE_MEMBERSHIP + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_ADD_SID_HISTORY + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_POST_BETA3 + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_GET_MEMBERSHIPS2 + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V6 + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_NONDOMAIN_NCS + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V8 + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_GETCHGREPLY_V5 + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_GETCHGREPLY_V6 + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_ADDENTRYREPLY_V3 + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_GETCHGREPLY_V7 + bind_info.info.supported_extensions |= drsuapi.DRSUAPI_SUPPORTED_EXTENSION_VERIFY_OBJECT + (info, handle) = drs.DsBind(misc.GUID(drsuapi.DRSUAPI_DS_BIND_GUID), bind_info) + + return (handle, info.info.supported_extensions) + + +class drs_Replicate: + '''DRS replication calls''' + + def __init__(self, binding_string, lp, creds, samdb): + self.drs = drsuapi.drsuapi(binding_string, lp, creds) + (self.drs_handle, self.supported_extensions) = drs_DsBind(self.drs) + self.net = Net(creds=creds, lp=lp) + self.samdb = samdb + self.replication_state = self.net.replicate_init(self.samdb, lp, self.drs) + + def drs_get_rodc_partial_attribute_set(self): + '''get a list of attributes for RODC replication''' + partial_attribute_set = drsuapi.DsPartialAttributeSet() + partial_attribute_set.version = 1 + + attids = [] + + # the exact list of attids we send is quite critical. Note that + # we do ask for the secret attributes, but set SPECIAL_SECRET_PROCESSING + # to zero them out + schema_dn = self.samdb.get_schema_basedn() + res = self.samdb.search(base=schema_dn, scope=ldb.SCOPE_SUBTREE, + expression="objectClass=attributeSchema", + attrs=["lDAPDisplayName", "systemFlags", + "searchFlags"]) + + for r in res: + ldap_display_name = r["lDAPDisplayName"][0] + if "systemFlags" in r: + system_flags = r["systemFlags"][0] + if (int(system_flags) & (samba.dsdb.DS_FLAG_ATTR_NOT_REPLICATED | + samba.dsdb.DS_FLAG_ATTR_IS_CONSTRUCTED)): + continue + if "searchFlags" in r: + search_flags = r["searchFlags"][0] + if (int(search_flags) & samba.dsdb.SEARCH_FLAG_RODC_ATTRIBUTE): + continue + attid = self.samdb.get_attid_from_lDAPDisplayName(ldap_display_name) + attids.append(int(attid)) + + # the attids do need to be sorted, or windows doesn't return + # all the attributes we need + attids.sort() + partial_attribute_set.attids = attids + partial_attribute_set.num_attids = len(attids) + return partial_attribute_set + + def replicate(self, dn, source_dsa_invocation_id, destination_dsa_guid, + schema=False, exop=drsuapi.DRSUAPI_EXOP_NONE, rodc=False, + replica_flags=None): + '''replicate a single DN''' + + # setup for a GetNCChanges call + req8 = drsuapi.DsGetNCChangesRequest8() + + req8.destination_dsa_guid = destination_dsa_guid + req8.source_dsa_invocation_id = source_dsa_invocation_id + req8.naming_context = drsuapi.DsReplicaObjectIdentifier() + req8.naming_context.dn = dn + req8.highwatermark = drsuapi.DsReplicaHighWaterMark() + req8.highwatermark.tmp_highest_usn = 0 + req8.highwatermark.reserved_usn = 0 + req8.highwatermark.highest_usn = 0 + req8.uptodateness_vector = None + if replica_flags is not None: + req8.replica_flags = replica_flags + elif exop == drsuapi.DRSUAPI_EXOP_REPL_SECRET: + req8.replica_flags = 0 + else: + req8.replica_flags = (drsuapi.DRSUAPI_DRS_INIT_SYNC | + drsuapi.DRSUAPI_DRS_PER_SYNC | + drsuapi.DRSUAPI_DRS_GET_ANC | + drsuapi.DRSUAPI_DRS_NEVER_SYNCED) + if rodc: + req8.replica_flags |= drsuapi.DRSUAPI_DRS_SPECIAL_SECRET_PROCESSING + else: + req8.replica_flags |= drsuapi.DRSUAPI_DRS_WRIT_REP + req8.max_object_count = 402 + req8.max_ndr_size = 402116 + req8.extended_op = exop + req8.fsmo_info = 0 + req8.partial_attribute_set = None + req8.partial_attribute_set_ex = None + req8.mapping_ctr.num_mappings = 0 + req8.mapping_ctr.mappings = None + + if not schema and rodc: + req8.partial_attribute_set = self.drs_get_rodc_partial_attribute_set() + + if self.supported_extensions & drsuapi.DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V8: + req_level = 8 + req = req8 + else: + req_level = 5 + req5 = drsuapi.DsGetNCChangesRequest5() + for a in dir(req5): + if a[0] != '_': + setattr(req5, a, getattr(req8, a)) + req = req5 + + + while True: + (level, ctr) = self.drs.DsGetNCChanges(self.drs_handle, req_level, req) + if ctr.first_object == None and ctr.object_count != 0: + raise RuntimeError("DsGetNCChanges: NULL first_object with object_count=%u" % (ctr.object_count)) + self.net.replicate_chunk(self.replication_state, level, ctr, schema=schema) + if ctr.more_data == 0: + break + req.highwatermark.tmp_highest_usn = ctr.new_highwatermark.tmp_highest_usn diff --git a/source4/scripting/python/samba/getopt.py b/source4/scripting/python/samba/getopt.py index 8b756b2d6f..671142b552 100644 --- a/source4/scripting/python/samba/getopt.py +++ b/source4/scripting/python/samba/getopt.py @@ -1,38 +1,56 @@ -#!/usr/bin/python +#!/usr/bin/env python # Samba-specific bits for optparse # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007 -# +# # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. -# +# # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # """Support for parsing Samba-related command-line options.""" -import optparse -from credentials import Credentials, DONT_USE_KERBEROS, MUST_USE_KERBEROS -from hostconfig import Hostconfig - __docformat__ = "restructuredText" +import optparse, os +from samba.credentials import ( + Credentials, + DONT_USE_KERBEROS, + MUST_USE_KERBEROS, + ) +from samba.hostconfig import Hostconfig +import sys + + class SambaOptions(optparse.OptionGroup): """General Samba-related command line options.""" + def __init__(self, parser): + from samba.param import LoadParm optparse.OptionGroup.__init__(self, parser, "Samba Common Options") self.add_option("-s", "--configfile", action="callback", - type=str, metavar="FILE", help="Configuration file", + type=str, metavar="FILE", help="Configuration file", callback=self._load_configfile) + self.add_option("-d", "--debuglevel", action="callback", + type=int, metavar="DEBUGLEVEL", help="debug level", + callback=self._set_debuglevel) + self.add_option("--option", action="callback", + type=str, metavar="OPTION", help="set smb.conf option from command line", + callback=self._set_option) + self.add_option("--realm", action="callback", + type=str, metavar="REALM", help="set the realm name", + callback=self._set_realm) self._configfile = None + self._lp = LoadParm() def get_loadparm_path(self): """Return the path to the smb.conf file specified on the command line. """ @@ -41,17 +59,28 @@ class SambaOptions(optparse.OptionGroup): def _load_configfile(self, option, opt_str, arg, parser): self._configfile = arg + def _set_debuglevel(self, option, opt_str, arg, parser): + self._lp.set('debug level', str(arg)) + + def _set_realm(self, option, opt_str, arg, parser): + self._lp.set('realm', arg) + + def _set_option(self, option, opt_str, arg, parser): + if arg.find('=') == -1: + print("--option takes a 'a=b' argument") + sys.exit(1) + a = arg.split('=') + self._lp.set(a[0], a[1]) + def get_loadparm(self): """Return a loadparm object with data specified on the command line. """ - import os, param - lp = param.LoadParm() if self._configfile is not None: - lp.load(self._configfile) + self._lp.load(self._configfile) elif os.getenv("SMB_CONF_PATH") is not None: - lp.load(os.getenv("SMB_CONF_PATH")) + self._lp.load(os.getenv("SMB_CONF_PATH")) else: - lp.load_default() - return lp + self._lp.load_default() + return self._lp def get_hostconfig(self): return Hostconfig(self.get_loadparm()) @@ -61,29 +90,41 @@ class VersionOptions(optparse.OptionGroup): """Command line option for printing Samba version.""" def __init__(self, parser): optparse.OptionGroup.__init__(self, parser, "Version Options") + self.add_option("--version", action="callback", + callback=self._display_version, + help="Display version number") + + def _display_version(self, option, opt_str, arg, parser): + import samba + print samba.version + sys.exit(0) class CredentialsOptions(optparse.OptionGroup): """Command line options for specifying credentials.""" def __init__(self, parser): - self.no_pass = False + self.no_pass = True + self.ipaddress = None optparse.OptionGroup.__init__(self, parser, "Credentials Options") self.add_option("--simple-bind-dn", metavar="DN", action="callback", callback=self._set_simple_bind_dn, type=str, help="DN to use for a simple bind") self.add_option("--password", metavar="PASSWORD", action="callback", help="Password", type=str, callback=self._set_password) - self.add_option("-U", "--username", metavar="USERNAME", + self.add_option("-U", "--username", metavar="USERNAME", action="callback", type=str, help="Username", callback=self._parse_username) - self.add_option("-W", "--workgroup", metavar="WORKGROUP", + self.add_option("-W", "--workgroup", metavar="WORKGROUP", action="callback", type=str, help="Workgroup", callback=self._parse_workgroup) self.add_option("-N", "--no-pass", action="store_true", help="Don't ask for a password") - self.add_option("-k", "--kerberos", metavar="KERBEROS", + self.add_option("-k", "--kerberos", metavar="KERBEROS", action="callback", type=str, help="Use Kerberos", callback=self._set_kerberos) + self.add_option("", "--ipaddress", metavar="IPADDRESS", + action="callback", type=str, + help="IP address of server", callback=self._set_ipaddress) self.creds = Credentials() def _parse_username(self, option, opt_str, arg, parser): @@ -94,23 +135,96 @@ class CredentialsOptions(optparse.OptionGroup): def _set_password(self, option, opt_str, arg, parser): self.creds.set_password(arg) + self.no_pass = False + + def _set_ipaddress(self, option, opt_str, arg, parser): + self.ipaddress = arg def _set_kerberos(self, option, opt_str, arg, parser): - if bool(arg) or arg.lower() == "yes": + if arg.lower() in ["yes", 'true', '1']: self.creds.set_kerberos_state(MUST_USE_KERBEROS) - else: + elif arg.lower() in ["no", 'false', '0']: self.creds.set_kerberos_state(DONT_USE_KERBEROS) + else: + raise optparse.BadOptionErr("invalid kerberos option: %s" % arg) def _set_simple_bind_dn(self, option, opt_str, arg, parser): self.creds.set_bind_dn(arg) - def get_credentials(self, lp): + def get_credentials(self, lp, fallback_machine=False): """Obtain the credentials set on the command-line. :param lp: Loadparm object to use. :return: Credentials object """ self.creds.guess(lp) - if not self.no_pass: + if self.no_pass: self.creds.set_cmdline_callbacks() + + # possibly fallback to using the machine account, if we have + # access to the secrets db + if fallback_machine and not self.creds.authentication_requested(): + try: + self.creds.set_machine_account(lp) + except Exception: + pass + return self.creds + +class CredentialsOptionsDouble(CredentialsOptions): + """Command line options for specifying credentials of two servers.""" + def __init__(self, parser): + CredentialsOptions.__init__(self, parser) + self.no_pass2 = True + self.add_option("--simple-bind-dn2", metavar="DN2", action="callback", + callback=self._set_simple_bind_dn2, type=str, + help="DN to use for a simple bind") + self.add_option("--password2", metavar="PASSWORD2", action="callback", + help="Password", type=str, callback=self._set_password2) + self.add_option("--username2", metavar="USERNAME2", + action="callback", type=str, + help="Username for second server", callback=self._parse_username2) + self.add_option("--workgroup2", metavar="WORKGROUP2", + action="callback", type=str, + help="Workgroup for second server", callback=self._parse_workgroup2) + self.add_option("--no-pass2", action="store_true", + help="Don't ask for a password for the second server") + self.add_option("--kerberos2", metavar="KERBEROS2", + action="callback", type=str, + help="Use Kerberos", callback=self._set_kerberos2) + self.creds2 = Credentials() + + def _parse_username2(self, option, opt_str, arg, parser): + self.creds2.parse_string(arg) + + def _parse_workgroup2(self, option, opt_str, arg, parser): + self.creds2.set_domain(arg) + + def _set_password2(self, option, opt_str, arg, parser): + self.creds2.set_password(arg) + self.no_pass2 = False + + def _set_kerberos2(self, option, opt_str, arg, parser): + if bool(arg) or arg.lower() == "yes": + self.creds2.set_kerberos_state(MUST_USE_KERBEROS) + else: + self.creds2.set_kerberos_state(DONT_USE_KERBEROS) + + def _set_simple_bind_dn2(self, option, opt_str, arg, parser): + self.creds2.set_bind_dn(arg) + + def get_credentials2(self, lp, guess=True): + """Obtain the credentials set on the command-line. + + :param lp: Loadparm object to use. + :param guess: Try guess Credentials from environment + :return: Credentials object + """ + if guess: + self.creds2.guess(lp) + elif not self.creds2.get_username(): + self.creds2.set_anonymous() + + if self.no_pass2: + self.creds2.set_cmdline_callbacks() + return self.creds2 diff --git a/source4/scripting/python/samba/hostconfig.py b/source4/scripting/python/samba/hostconfig.py index 313e3420b0..3e6dc6b1dd 100644 --- a/source4/scripting/python/samba/hostconfig.py +++ b/source4/scripting/python/samba/hostconfig.py @@ -1,33 +1,83 @@ -#!/usr/bin/python +#!/usr/bin/env python # Unix SMB/CIFS implementation. # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2008 -# +# # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. -# +# # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # +"""Local host configuration.""" + from samdb import SamDB class Hostconfig(object): - """Aggregate object that contains all information about the configuration + """Aggregate object that contains all information about the configuration of a Samba host.""" - def __init__(self, lp): + def __init__(self, lp): self.lp = lp + def get_shares(self): + return SharesContainer(self.lp) + def get_samdb(self, session_info, credentials): - return SamDB(url=self.lp.get("sam database"), - session_info=session_info, credentials=credentials, + """Access the SamDB host. + + :param session_info: Session info to use + :param credentials: Credentials to access the SamDB with + """ + return SamDB(url=self.lp.get("sam database"), + session_info=session_info, credentials=credentials, lp=self.lp) + +# TODO: Rather than accessing Loadparm directly here, we should really +# have bindings to the param/shares.c and use those. + + +class SharesContainer(object): + """A shares container.""" + + def __init__(self, lp): + self._lp = lp + + def __getitem__(self, name): + if name == "global": + # [global] is not a share + raise KeyError + return Share(self._lp[name]) + + def __len__(self): + if "global" in self._lp.services(): + return len(self._lp)-1 + return len(self._lp) + + def keys(self): + return [name for name in self._lp.services() if name != "global"] + + def __iter__(self): + return iter(self.keys()) + + +class Share(object): + """A file share.""" + + def __init__(self, service): + self._service = service + + def __getitem__(self, name): + return self._service[name] + + def __setitem__(self, name, value): + self._service[name] = value diff --git a/source4/scripting/python/samba/idmap.py b/source4/scripting/python/samba/idmap.py index ad209f42de..93fca46edd 100644 --- a/source4/scripting/python/samba/idmap.py +++ b/source4/scripting/python/samba/idmap.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # Unix SMB/CIFS implementation. # Copyright (C) 2008 Kai Blin <kai@samba.org> @@ -22,6 +22,7 @@ __docformat__ = "restructuredText" +import ldb import samba class IDmapDB(samba.Ldb): @@ -50,12 +51,34 @@ class IDmapDB(samba.Ldb): super(IDmapDB, self).connect(url=self.lp.private_path(url), flags=flags, options=options) - def setup_name_mapping(self, sid, type, unixid): + + def increment_xid(self): + """Increment xidNumber, if not present it create and assign it to the lowerBound + + :return xid can that be used for SID/unixid mapping + """ + res = self.search(expression="dn=CN=CONFIG", base="", + scope=ldb.SCOPE_SUBTREE) + id = res[0].get("xidNumber") + flag = ldb.FLAG_MOD_REPLACE + if id is None: + id = res[0].get("lowerBound") + flag = ldb.FLAG_MOD_ADD + newid = int(str(id)) + 1 + msg = ldb.Message() + msg.dn = ldb.Dn(self, "CN=CONFIG") + msg["xidNumber"] = ldb.MessageElement(str(newid), flag, "xidNumber") + self.modify(msg) + return id + + def setup_name_mapping(self, sid, type, unixid=None): """Setup a mapping between a sam name and a unix name. :param sid: SID of the NT-side of the mapping. - :param unixname: Unix name to map to. + :param unixname: Unix id to map to, if none supplied the next one will be selected """ + if unixid is None: + unixid = self.increment_xid() type_string = "" if type == self.TYPE_UID: type_string = "ID_TYPE_UID" diff --git a/source4/scripting/python/samba/join.py b/source4/scripting/python/samba/join.py new file mode 100644 index 0000000000..401f262154 --- /dev/null +++ b/source4/scripting/python/samba/join.py @@ -0,0 +1,582 @@ +#!/usr/bin/env python +# +# python join code +# Copyright Andrew Tridgell 2010 +# Copyright Andrew Bartlett 2010 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +"""Joining a domain.""" + +from samba.auth import system_session +from samba.samdb import SamDB +from samba import gensec, Ldb, drs_utils +import ldb, samba, sys, os, uuid +from samba.ndr import ndr_pack +from samba.dcerpc import security, drsuapi, misc, nbt +from samba.credentials import Credentials, DONT_USE_KERBEROS +from samba.provision import secretsdb_self_join, provision, FILL_DRS +from samba.schema import Schema +from samba.net import Net +import logging +import talloc + +# this makes debugging easier +talloc.enable_null_tracking() + + +class dc_join(object): + '''perform a DC join''' + + def __init__(ctx, server=None, creds=None, lp=None, site=None, + netbios_name=None, targetdir=None, domain=None): + ctx.creds = creds + ctx.lp = lp + ctx.site = site + ctx.netbios_name = netbios_name + ctx.targetdir = targetdir + + ctx.creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL) + ctx.net = Net(creds=ctx.creds, lp=ctx.lp) + + if server is not None: + ctx.server = server + else: + print("Finding a writeable DC for domain '%s'" % domain) + ctx.server = ctx.find_dc(domain) + print("Found DC %s" % ctx.server) + + ctx.samdb = SamDB(url="ldap://%s" % ctx.server, + session_info=system_session(), + credentials=ctx.creds, lp=ctx.lp) + + ctx.myname = netbios_name + ctx.samname = "%s$" % ctx.myname + ctx.base_dn = str(ctx.samdb.get_default_basedn()) + ctx.root_dn = str(ctx.samdb.get_root_basedn()) + ctx.schema_dn = str(ctx.samdb.get_schema_basedn()) + ctx.config_dn = str(ctx.samdb.get_config_basedn()) + ctx.domsid = ctx.samdb.get_domain_sid() + ctx.domain_name = ctx.get_domain_name() + + lp.set("workgroup", ctx.domain_name) + print("workgroup is %s" % ctx.domain_name) + + ctx.dc_ntds_dn = ctx.get_dsServiceName() + ctx.dc_dnsHostName = ctx.get_dnsHostName() + ctx.behavior_version = ctx.get_behavior_version() + + ctx.acct_pass = samba.generate_random_password(32, 40) + + # work out the DNs of all the objects we will be adding + ctx.server_dn = "CN=%s,CN=Servers,CN=%s,CN=Sites,%s" % (ctx.myname, ctx.site, ctx.config_dn) + ctx.ntds_dn = "CN=NTDS Settings,%s" % ctx.server_dn + topology_base = "CN=Topology,CN=Domain System Volume,CN=DFSR-GlobalSettings,CN=System,%s" % ctx.base_dn + if ctx.dn_exists(topology_base): + ctx.topology_dn = "CN=%s,%s" % (ctx.myname, topology_base) + else: + ctx.topology_dn = None + + ctx.dnsdomain = ldb.Dn(ctx.samdb, ctx.base_dn).canonical_str().split('/')[0] + + ctx.realm = ctx.dnsdomain + lp.set("realm", ctx.realm) + + print("realm is %s" % ctx.realm) + + ctx.dnshostname = "%s.%s" % (ctx.myname.lower(), ctx.dnsdomain) + + ctx.acct_dn = "CN=%s,OU=Domain Controllers,%s" % (ctx.myname, ctx.base_dn) + + ctx.tmp_samdb = None + + ctx.SPNs = [ "HOST/%s" % ctx.myname, + "HOST/%s" % ctx.dnshostname, + "GC/%s/%s" % (ctx.dnshostname, ctx.dnsdomain) ] + + # these elements are optional + ctx.never_reveal_sid = None + ctx.reveal_sid = None + ctx.connection_dn = None + ctx.RODC = False + ctx.krbtgt_dn = None + ctx.drsuapi = None + ctx.managedby = None + + + def del_noerror(ctx, dn, recursive=False): + if recursive: + try: + res = ctx.samdb.search(base=dn, scope=ldb.SCOPE_ONELEVEL, attrs=["dn"]) + except Exception: + return + for r in res: + ctx.del_noerror(r.dn, recursive=True) + try: + ctx.samdb.delete(dn) + print "Deleted %s" % dn + except Exception: + pass + + def cleanup_old_join(ctx): + '''remove any DNs from a previous join''' + try: + # find the krbtgt link + print("checking samaccountname") + res = ctx.samdb.search(base=ctx.samdb.get_default_basedn(), + expression='samAccountName=%s' % ctx.samname, + attrs=["msDS-krbTgtLink"]) + if res: + ctx.del_noerror(res[0].dn, recursive=True) + if ctx.connection_dn is not None: + ctx.del_noerror(ctx.connection_dn) + if ctx.krbtgt_dn is not None: + ctx.del_noerror(ctx.krbtgt_dn) + ctx.del_noerror(ctx.ntds_dn) + ctx.del_noerror(ctx.server_dn, recursive=True) + if ctx.topology_dn: + ctx.del_noerror(ctx.topology_dn) + if res: + ctx.new_krbtgt_dn = res[0]["msDS-Krbtgtlink"][0] + ctx.del_noerror(ctx.new_krbtgt_dn) + except Exception: + pass + + def find_dc(ctx, domain): + '''find a writeable DC for the given domain''' + try: + ctx.cldap_ret = ctx.net.finddc(domain, nbt.NBT_SERVER_LDAP | nbt.NBT_SERVER_DS | nbt.NBT_SERVER_WRITABLE) + except Exception: + raise Exception("Failed to find a writeable DC for domain '%s'" % domain) + if ctx.cldap_ret.client_site is not None and ctx.cldap_ret.client_site != "": + ctx.site = ctx.cldap_ret.client_site + return ctx.cldap_ret.pdc_dns_name + + + def get_dsServiceName(ctx): + res = ctx.samdb.search(base="", scope=ldb.SCOPE_BASE, attrs=["dsServiceName"]) + return res[0]["dsServiceName"][0] + + def get_behavior_version(ctx): + res = ctx.samdb.search(base=ctx.base_dn, scope=ldb.SCOPE_BASE, attrs=["msDS-Behavior-Version"]) + if "msDS-Behavior-Version" in res[0]: + return int(res[0]["msDS-Behavior-Version"][0]) + else: + return samba.dsdb.DS_DOMAIN_FUNCTION_2000 + + def get_dnsHostName(ctx): + res = ctx.samdb.search(base="", scope=ldb.SCOPE_BASE, attrs=["dnsHostName"]) + return res[0]["dnsHostName"][0] + + def get_domain_name(ctx): + '''get netbios name of the domain from the partitions record''' + partitions_dn = ctx.samdb.get_partitions_dn() + res = ctx.samdb.search(base=partitions_dn, scope=ldb.SCOPE_ONELEVEL, attrs=["nETBIOSName"], + expression='ncName=%s' % ctx.samdb.get_default_basedn()) + return res[0]["nETBIOSName"][0] + + def get_mysid(ctx): + '''get the SID of the connected user. Only works with w2k8 and later, + so only used for RODC join''' + res = ctx.samdb.search(base="", scope=ldb.SCOPE_BASE, attrs=["tokenGroups"]) + binsid = res[0]["tokenGroups"][0] + return ctx.samdb.schema_format_value("objectSID", binsid) + + def dn_exists(ctx, dn): + '''check if a DN exists''' + try: + res = ctx.samdb.search(base=dn, scope=ldb.SCOPE_BASE, attrs=[]) + except ldb.LdbError, (enum, estr): + if enum == ldb.ERR_NO_SUCH_OBJECT: + return False + raise + return True + + def add_krbtgt_account(ctx): + '''RODCs need a special krbtgt account''' + print "Adding %s" % ctx.krbtgt_dn + rec = { + "dn" : ctx.krbtgt_dn, + "objectclass" : "user", + "useraccountcontrol" : str(samba.dsdb.UF_NORMAL_ACCOUNT | + samba.dsdb.UF_ACCOUNTDISABLE), + "showinadvancedviewonly" : "TRUE", + "description" : "krbtgt for %s" % ctx.samname} + ctx.samdb.add(rec, ["rodc_join:1:1"]) + + # now we need to search for the samAccountName attribute on the krbtgt DN, + # as this will have been magically set to the krbtgt number + res = ctx.samdb.search(base=ctx.krbtgt_dn, scope=ldb.SCOPE_BASE, attrs=["samAccountName"]) + ctx.krbtgt_name = res[0]["samAccountName"][0] + + print "Got krbtgt_name=%s" % ctx.krbtgt_name + + m = ldb.Message() + m.dn = ldb.Dn(ctx.samdb, ctx.acct_dn) + m["msDS-krbTgtLink"] = ldb.MessageElement(ctx.krbtgt_dn, + ldb.FLAG_MOD_REPLACE, "msDS-krbTgtLink") + ctx.samdb.modify(m) + + ctx.new_krbtgt_dn = "CN=%s,CN=Users,%s" % (ctx.krbtgt_name, ctx.base_dn) + print "Renaming %s to %s" % (ctx.krbtgt_dn, ctx.new_krbtgt_dn) + ctx.samdb.rename(ctx.krbtgt_dn, ctx.new_krbtgt_dn) + + def drsuapi_connect(ctx): + '''make a DRSUAPI connection to the server''' + binding_options = "seal" + if ctx.lp.get("log level") >= 5: + binding_options += ",print" + binding_string = "ncacn_ip_tcp:%s[%s]" % (ctx.server, binding_options) + ctx.drsuapi = drsuapi.drsuapi(binding_string, ctx.lp, ctx.creds) + (ctx.drsuapi_handle, ctx.bind_supported_extensions) = drs_utils.drs_DsBind(ctx.drsuapi) + + def create_tmp_samdb(ctx): + '''create a temporary samdb object for schema queries''' + ctx.tmp_schema = Schema(security.dom_sid(ctx.domsid), + schemadn=ctx.schema_dn) + ctx.tmp_samdb = SamDB(session_info=system_session(), url=None, auto_connect=False, + credentials=ctx.creds, lp=ctx.lp, global_schema=False, + am_rodc=False) + ctx.tmp_samdb.set_schema(ctx.tmp_schema) + + def build_DsReplicaAttribute(ctx, attrname, attrvalue): + '''build a DsReplicaAttributeCtr object''' + r = drsuapi.DsReplicaAttribute() + r.attid = ctx.tmp_samdb.get_attid_from_lDAPDisplayName(attrname) + r.value_ctr = 1 + + + def DsAddEntry(ctx, rec): + '''add a record via the DRSUAPI DsAddEntry call''' + if ctx.drsuapi is None: + ctx.drsuapi_connect() + if ctx.tmp_samdb is None: + ctx.create_tmp_samdb() + + id = drsuapi.DsReplicaObjectIdentifier() + id.dn = rec['dn'] + + attrs = [] + for a in rec: + if a == 'dn': + continue + if not isinstance(rec[a], list): + v = [rec[a]] + else: + v = rec[a] + rattr = ctx.tmp_samdb.dsdb_DsReplicaAttribute(ctx.tmp_samdb, a, v) + attrs.append(rattr) + + attribute_ctr = drsuapi.DsReplicaAttributeCtr() + attribute_ctr.num_attributes = len(attrs) + attribute_ctr.attributes = attrs + + object = drsuapi.DsReplicaObject() + object.identifier = id + object.attribute_ctr = attribute_ctr + + first_object = drsuapi.DsReplicaObjectListItem() + first_object.object = object + + req2 = drsuapi.DsAddEntryRequest2() + req2.first_object = first_object + + (level, ctr) = ctx.drsuapi.DsAddEntry(ctx.drsuapi_handle, 2, req2) + if ctr.err_ver != 1: + raise RuntimeError("expected err_ver 1, got %u" % ctr.err_ver) + if ctr.err_data.status != (0, 'WERR_OK'): + print("DsAddEntry failed with status %s info %s" % (ctr.err_data.status, + ctr.err_data.info.extended_err)) + raise RuntimeError("DsAddEntry failed") + + def join_add_objects(ctx): + '''add the various objects needed for the join''' + print "Adding %s" % ctx.acct_dn + rec = { + "dn" : ctx.acct_dn, + "objectClass": "computer", + "displayname": ctx.samname, + "samaccountname" : ctx.samname, + "userAccountControl" : str(ctx.userAccountControl | samba.dsdb.UF_ACCOUNTDISABLE), + "dnshostname" : ctx.dnshostname} + if ctx.behavior_version >= samba.dsdb.DS_DOMAIN_FUNCTION_2008: + rec['msDS-SupportedEncryptionTypes'] = str(samba.dsdb.ENC_ALL_TYPES) + if ctx.managedby: + rec["managedby"] = ctx.managedby + if ctx.never_reveal_sid: + rec["msDS-NeverRevealGroup"] = ctx.never_reveal_sid + if ctx.reveal_sid: + rec["msDS-RevealOnDemandGroup"] = ctx.reveal_sid + ctx.samdb.add(rec) + + if ctx.krbtgt_dn: + ctx.add_krbtgt_account() + + print "Adding %s" % ctx.server_dn + rec = { + "dn": ctx.server_dn, + "objectclass" : "server", + "systemFlags" : str(samba.dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME | + samba.dsdb.SYSTEM_FLAG_CONFIG_ALLOW_LIMITED_MOVE | + samba.dsdb.SYSTEM_FLAG_DISALLOW_MOVE_ON_DELETE), + "serverReference" : ctx.acct_dn, + "dnsHostName" : ctx.dnshostname} + ctx.samdb.add(rec) + + # FIXME: the partition (NC) assignment has to be made dynamic + print "Adding %s" % ctx.ntds_dn + rec = { + "dn" : ctx.ntds_dn, + "objectclass" : "nTDSDSA", + "systemFlags" : str(samba.dsdb.SYSTEM_FLAG_DISALLOW_MOVE_ON_DELETE), + "dMDLocation" : ctx.schema_dn} + + if ctx.behavior_version >= samba.dsdb.DS_DOMAIN_FUNCTION_2003: + rec["msDS-Behavior-Version"] = str(ctx.behavior_version) + + if ctx.behavior_version >= samba.dsdb.DS_DOMAIN_FUNCTION_2003: + rec["msDS-HasDomainNCs"] = ctx.base_dn + + if ctx.RODC: + rec["objectCategory"] = "CN=NTDS-DSA-RO,%s" % ctx.schema_dn + rec["msDS-HasFullReplicaNCs"] = [ ctx.base_dn, ctx.config_dn, ctx.schema_dn ] + rec["options"] = "37" + ctx.samdb.add(rec, ["rodc_join:1:1"]) + else: + rec["objectCategory"] = "CN=NTDS-DSA,%s" % ctx.schema_dn + rec["HasMasterNCs"] = [ ctx.base_dn, ctx.config_dn, ctx.schema_dn ] + if ctx.behavior_version >= samba.dsdb.DS_DOMAIN_FUNCTION_2003: + rec["msDS-HasMasterNCs"] = [ ctx.base_dn, ctx.config_dn, ctx.schema_dn ] + rec["options"] = "1" + rec["invocationId"] = ndr_pack(misc.GUID(str(uuid.uuid4()))) + ctx.DsAddEntry(rec) + + # find the GUID of our NTDS DN + res = ctx.samdb.search(base=ctx.ntds_dn, scope=ldb.SCOPE_BASE, attrs=["objectGUID"]) + ctx.ntds_guid = misc.GUID(ctx.samdb.schema_format_value("objectGUID", res[0]["objectGUID"][0])) + + if ctx.connection_dn is not None: + print "Adding %s" % ctx.connection_dn + rec = { + "dn" : ctx.connection_dn, + "objectclass" : "nTDSConnection", + "enabledconnection" : "TRUE", + "options" : "65", + "fromServer" : ctx.dc_ntds_dn} + ctx.samdb.add(rec) + + if ctx.topology_dn: + print "Adding %s" % ctx.topology_dn + rec = { + "dn" : ctx.topology_dn, + "objectclass" : "msDFSR-Member", + "msDFSR-ComputerReference" : ctx.acct_dn, + "serverReference" : ctx.ntds_dn} + ctx.samdb.add(rec) + + print "Adding SPNs to %s" % ctx.acct_dn + m = ldb.Message() + m.dn = ldb.Dn(ctx.samdb, ctx.acct_dn) + for i in range(len(ctx.SPNs)): + ctx.SPNs[i] = ctx.SPNs[i].replace("$NTDSGUID", str(ctx.ntds_guid)) + m["servicePrincipalName"] = ldb.MessageElement(ctx.SPNs, + ldb.FLAG_MOD_ADD, + "servicePrincipalName") + ctx.samdb.modify(m) + + print "Setting account password for %s" % ctx.samname + ctx.samdb.setpassword("(&(objectClass=user)(sAMAccountName=%s))" % ctx.samname, + ctx.acct_pass, + force_change_at_next_login=False, + username=ctx.samname) + res = ctx.samdb.search(base=ctx.acct_dn, scope=ldb.SCOPE_BASE, attrs=["msDS-keyVersionNumber"]) + ctx.key_version_number = int(res[0]["msDS-keyVersionNumber"][0]) + + print("Enabling account") + m = ldb.Message() + m.dn = ldb.Dn(ctx.samdb, ctx.acct_dn) + m["userAccountControl"] = ldb.MessageElement(str(ctx.userAccountControl), + ldb.FLAG_MOD_REPLACE, + "userAccountControl") + ctx.samdb.modify(m) + + def join_provision(ctx): + '''provision the local SAM''' + + print "Calling bare provision" + + logger = logging.getLogger("provision") + logger.addHandler(logging.StreamHandler(sys.stdout)) + smbconf = ctx.lp.configfile + + presult = provision(logger, system_session(), None, + smbconf=smbconf, targetdir=ctx.targetdir, samdb_fill=FILL_DRS, + realm=ctx.realm, rootdn=ctx.root_dn, domaindn=ctx.base_dn, + schemadn=ctx.schema_dn, + configdn=ctx.config_dn, + serverdn=ctx.server_dn, domain=ctx.domain_name, + hostname=ctx.myname, domainsid=ctx.domsid, + machinepass=ctx.acct_pass, serverrole="domain controller", + sitename=ctx.site, lp=ctx.lp) + print "Provision OK for domain DN %s" % presult.domaindn + ctx.local_samdb = presult.samdb + ctx.lp = presult.lp + ctx.paths = presult.paths + + + def join_replicate(ctx): + '''replicate the SAM''' + + print "Starting replication" + ctx.local_samdb.transaction_start() + try: + source_dsa_invocation_id = misc.GUID(ctx.samdb.get_invocation_id()) + destination_dsa_guid = ctx.ntds_guid + + if ctx.RODC: + repl_creds = Credentials() + repl_creds.guess(ctx.lp) + repl_creds.set_kerberos_state(DONT_USE_KERBEROS) + repl_creds.set_username(ctx.samname) + repl_creds.set_password(ctx.acct_pass) + else: + repl_creds = ctx.creds + + binding_options = "seal" + if ctx.lp.get("debug level") >= 5: + binding_options += ",print" + repl = drs_utils.drs_Replicate( + "ncacn_ip_tcp:%s[%s]" % (ctx.server, binding_options), + ctx.lp, repl_creds, ctx.local_samdb) + + repl.replicate(ctx.schema_dn, source_dsa_invocation_id, + destination_dsa_guid, schema=True, rodc=ctx.RODC, + replica_flags=ctx.replica_flags) + repl.replicate(ctx.config_dn, source_dsa_invocation_id, + destination_dsa_guid, rodc=ctx.RODC, + replica_flags=ctx.replica_flags) + repl.replicate(ctx.base_dn, source_dsa_invocation_id, + destination_dsa_guid, rodc=ctx.RODC, + replica_flags=ctx.replica_flags) + if ctx.RODC: + repl.replicate(ctx.acct_dn, source_dsa_invocation_id, + destination_dsa_guid, + exop=drsuapi.DRSUAPI_EXOP_REPL_SECRET, rodc=True) + repl.replicate(ctx.new_krbtgt_dn, source_dsa_invocation_id, + destination_dsa_guid, + exop=drsuapi.DRSUAPI_EXOP_REPL_SECRET, rodc=True) + + print "Committing SAM database" + except: + ctx.local_samdb.transaction_cancel() + raise + else: + ctx.local_samdb.transaction_commit() + + + def join_finalise(ctx): + '''finalise the join, mark us synchronised and setup secrets db''' + + print "Setting isSynchronized" + m = ldb.Message() + m.dn = ldb.Dn(ctx.samdb, '@ROOTDSE') + m["isSynchronized"] = ldb.MessageElement("TRUE", ldb.FLAG_MOD_REPLACE, "isSynchronized") + ctx.samdb.modify(m) + + secrets_ldb = Ldb(ctx.paths.secrets, session_info=system_session(), lp=ctx.lp) + + print "Setting up secrets database" + secretsdb_self_join(secrets_ldb, domain=ctx.domain_name, + realm=ctx.realm, + dnsdomain=ctx.dnsdomain, + netbiosname=ctx.myname, + domainsid=security.dom_sid(ctx.domsid), + machinepass=ctx.acct_pass, + secure_channel_type=ctx.secure_channel_type, + key_version_number=ctx.key_version_number) + + def do_join(ctx): + ctx.cleanup_old_join() + try: + ctx.join_add_objects() + ctx.join_provision() + ctx.join_replicate() + ctx.join_finalise() + except Exception: + print "Join failed - cleaning up" + ctx.cleanup_old_join() + raise + + +def join_RODC(server=None, creds=None, lp=None, site=None, netbios_name=None, + targetdir=None, domain=None): + """join as a RODC""" + + ctx = dc_join(server, creds, lp, site, netbios_name, targetdir, domain) + + ctx.krbtgt_dn = "CN=krbtgt_%s,CN=Users,%s" % (ctx.myname, ctx.base_dn) + + # setup some defaults for accounts that should be replicated to this RODC + ctx.never_reveal_sid = [ "<SID=%s-%s>" % (ctx.domsid, security.DOMAIN_RID_RODC_DENY), + "<SID=%s>" % security.SID_BUILTIN_ADMINISTRATORS, + "<SID=%s>" % security.SID_BUILTIN_SERVER_OPERATORS, + "<SID=%s>" % security.SID_BUILTIN_BACKUP_OPERATORS, + "<SID=%s>" % security.SID_BUILTIN_ACCOUNT_OPERATORS ] + ctx.reveal_sid = "<SID=%s-%s>" % (ctx.domsid, security.DOMAIN_RID_RODC_ALLOW) + + mysid = ctx.get_mysid() + admin_dn = "<SID=%s>" % mysid + ctx.managedby = admin_dn + + ctx.userAccountControl = (samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT | + samba.dsdb.UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION | + samba.dsdb.UF_PARTIAL_SECRETS_ACCOUNT) + + ctx.SPNs.extend([ "RestrictedKrbHost/%s" % ctx.myname, + "RestrictedKrbHost/%s" % ctx.dnshostname ]) + + ctx.connection_dn = "CN=RODC Connection (FRS),%s" % ctx.ntds_dn + ctx.secure_channel_type = misc.SEC_CHAN_RODC + ctx.RODC = True + ctx.replica_flags = (drsuapi.DRSUAPI_DRS_INIT_SYNC | + drsuapi.DRSUAPI_DRS_PER_SYNC | + drsuapi.DRSUAPI_DRS_GET_ANC | + drsuapi.DRSUAPI_DRS_NEVER_SYNCED | + drsuapi.DRSUAPI_DRS_SPECIAL_SECRET_PROCESSING | + drsuapi.DRSUAPI_DRS_GET_ALL_GROUP_MEMBERSHIP) + ctx.do_join() + + + print "Joined domain %s (SID %s) as an RODC" % (ctx.domain_name, ctx.domsid) + + +def join_DC(server=None, creds=None, lp=None, site=None, netbios_name=None, + targetdir=None, domain=None): + """join as a DC""" + ctx = dc_join(server, creds, lp, site, netbios_name, targetdir, domain) + + ctx.userAccountControl = samba.dsdb.UF_SERVER_TRUST_ACCOUNT | samba.dsdb.UF_TRUSTED_FOR_DELEGATION + + ctx.SPNs.append('E3514235-4B06-11D1-AB04-00C04FC2DCD2/$NTDSGUID/%s' % ctx.dnsdomain) + ctx.secure_channel_type = misc.SEC_CHAN_BDC + + ctx.replica_flags = (drsuapi.DRSUAPI_DRS_WRIT_REP | + drsuapi.DRSUAPI_DRS_INIT_SYNC | + drsuapi.DRSUAPI_DRS_PER_SYNC | + drsuapi.DRSUAPI_DRS_FULL_SYNC_IN_PROGRESS | + drsuapi.DRSUAPI_DRS_NEVER_SYNCED) + + ctx.do_join() + print "Joined domain %s (SID %s) as a DC" % (ctx.domain_name, ctx.domsid) diff --git a/source4/scripting/python/samba/ms_display_specifiers.py b/source4/scripting/python/samba/ms_display_specifiers.py index 2a54e4ae0e..fd92b20e66 100644 --- a/source4/scripting/python/samba/ms_display_specifiers.py +++ b/source4/scripting/python/samba/ms_display_specifiers.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # # Create DisplaySpecifiers LDIF (as a string) from the documents provided by # Microsoft under the WSPP. diff --git a/source4/scripting/python/samba/ms_schema.py b/source4/scripting/python/samba/ms_schema.py index a0abc337ce..64bb28a967 100644 --- a/source4/scripting/python/samba/ms_schema.py +++ b/source4/scripting/python/samba/ms_schema.py @@ -1,12 +1,14 @@ -#!/usr/bin/env python # # create schema.ldif (as a string) from WSPP documentation # # based on minschema.py and minschema_wspp # +"""Generate LDIF from WSPP documentation.""" + import re import base64 +import uuid bitFields = {} @@ -33,19 +35,19 @@ bitFields["searchflags"] = { # 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 + '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 @@ -227,6 +229,9 @@ def __transform_entry(entry, objectClass): entry.insert(0, ["dn", "CN=%s,${SCHEMADN}" % cn]) entry.insert(1, ["objectClass", ["top", objectClass]]) entry.insert(2, ["cn", cn]) + entry.insert(2, ["objectGUID", str(uuid.uuid4())]) + entry.insert(2, ["adminDescription", cn]) + entry.insert(2, ["adminDisplayName", cn]) for l in entry: key = l[0].lower() @@ -272,5 +277,3 @@ if __name__ == '__main__': sys.exit(1) print read_ms_schema(attr_file, classes_file) - - diff --git a/source4/scripting/python/samba/ndr.py b/source4/scripting/python/samba/ndr.py index e718ff3422..112668523f 100644 --- a/source4/scripting/python/samba/ndr.py +++ b/source4/scripting/python/samba/ndr.py @@ -1,28 +1,49 @@ -#!/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/>. # + +"""Network Data Representation (NDR) marshalling and unmarshalling.""" + + def ndr_pack(object): - return object.__ndr_pack__() + """Pack a NDR object. + + :param object: Object to pack + :return: String object with marshalled object. + """ + ndr_pack = getattr(object, "__ndr_pack__", None) + if ndr_pack is None: + raise TypeError("%r is not a NDR object" % object) + return ndr_pack() def ndr_unpack(cls, data): + """NDR unpack an object. + + :param cls: Class of the object to unpack + :param data: Buffer to unpack + :return: Unpacked object + """ object = cls() object.__ndr_unpack__(data) return object + + +def ndr_print(object): + return object.__ndr_print__() diff --git a/source4/scripting/python/samba/netcmd/__init__.py b/source4/scripting/python/samba/netcmd/__init__.py new file mode 100644 index 0000000000..aa74f657b2 --- /dev/null +++ b/source4/scripting/python/samba/netcmd/__init__.py @@ -0,0 +1,215 @@ +#!/usr/bin/env 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/>. +# + +import optparse, samba +from samba import getopt as options +from ldb import LdbError +import sys, traceback + + +class Option(optparse.Option): + pass + + +class Command(object): + """A net command.""" + + def _get_description(self): + return self.__doc__.splitlines()[0].rstrip("\n") + + def _get_name(self): + name = self.__class__.__name__ + if name.startswith("cmd_"): + return name[4:] + return name + + name = property(_get_name) + + def usage(self, *args): + parser, _ = self._create_parser() + parser.print_usage() + + description = property(_get_description) + + def _get_synopsis(self): + ret = self.name + if self.takes_args: + ret += " " + " ".join([x.upper() for x in self.takes_args]) + return ret + + def show_command_error(self, e): + '''display a command error''' + if isinstance(e, CommandError): + (etype, evalue, etraceback) = e.exception_info + inner_exception = e.inner_exception + message = e.message + force_traceback = False + else: + (etype, evalue, etraceback) = sys.exc_info() + inner_exception = e + message = "uncaught exception" + force_traceback = True + + if isinstance(inner_exception, LdbError): + (ldb_ecode, ldb_emsg) = inner_exception + print >>sys.stderr, "ERROR(ldb): %s - %s" % (message, ldb_emsg) + elif isinstance(inner_exception, AssertionError): + print >>sys.stderr, "ERROR(assert): %s" % message + force_traceback = True + elif isinstance(inner_exception, RuntimeError): + print >>sys.stderr, "ERROR(runtime): %s - %s" % (message, evalue) + elif type(inner_exception) is Exception: + print >>sys.stderr, "ERROR(exception): %s - %s" % (message, evalue) + force_traceback = True + elif inner_exception is None: + print >>sys.stderr, "ERROR: %s" % (message) + else: + print >>sys.stderr, "ERROR(%s): %s - %s" % (str(etype), message, evalue) + force_traceback = True + + if force_traceback or samba.get_debug_level() >= 3: + traceback.print_tb(etraceback) + + + synopsis = property(_get_synopsis) + + outf = sys.stdout + + takes_args = [] + takes_options = [] + takes_optiongroups = {} + + def _create_parser(self): + parser = optparse.OptionParser(self.synopsis) + parser.prog = "net" + parser.add_options(self.takes_options) + optiongroups = {} + for name, optiongroup in self.takes_optiongroups.iteritems(): + optiongroups[name] = optiongroup(parser) + parser.add_option_group(optiongroups[name]) + return parser, optiongroups + + def message(self, text): + print text + + def _run(self, *argv): + parser, optiongroups = self._create_parser() + opts, args = parser.parse_args(list(argv)) + # Filter out options from option groups + args = args[1:] + kwargs = dict(opts.__dict__) + for option_group in parser.option_groups: + for option in option_group.option_list: + if option.dest is not None: + del kwargs[option.dest] + kwargs.update(optiongroups) + min_args = 0 + max_args = 0 + for i, arg in enumerate(self.takes_args): + if arg[-1] not in ("?", "*"): + min_args += 1 + max_args += 1 + if arg[-1] == "*": + max_args = -1 + if len(args) < min_args or (max_args != -1 and len(args) > max_args): + self.usage(*args) + return -1 + try: + return self.run(*args, **kwargs) + except Exception, e: + self.show_command_error(e) + return -1 + + def run(self): + """Run the command. This should be overriden by all subclasses.""" + raise NotImplementedError(self.run) + + +class SuperCommand(Command): + """A command with subcommands.""" + + subcommands = {} + + def _run(self, myname, subcommand=None, *args): + if subcommand in self.subcommands: + return self.subcommands[subcommand]._run(subcommand, *args) + print "Available subcommands:" + for cmd in self.subcommands: + print "\t%-20s - %s" % (cmd, self.subcommands[cmd].description) + if subcommand in [None, 'help', '-h', '--help' ]: + return 0 + raise CommandError("No such subcommand '%s'" % subcommand) + + def usage(self, myname, subcommand=None, *args): + if subcommand is None or not subcommand in self.subcommands: + print "Usage: %s (%s) [options]" % (myname, + " | ".join(self.subcommands.keys())) + else: + return self.subcommands[subcommand].usage(*args) + + +class CommandError(Exception): + '''an exception class for netcmd errors''' + def __init__(self, message, inner_exception=None): + self.message = message + self.inner_exception = inner_exception + self.exception_info = sys.exc_info() + + +commands = {} +from samba.netcmd.pwsettings import cmd_pwsettings +commands["pwsettings"] = cmd_pwsettings() +from samba.netcmd.domainlevel import cmd_domainlevel +commands["domainlevel"] = cmd_domainlevel() +from samba.netcmd.setpassword import cmd_setpassword +commands["setpassword"] = cmd_setpassword() +from samba.netcmd.setexpiry import cmd_setexpiry +commands["setexpiry"] = cmd_setexpiry() +from samba.netcmd.enableaccount import cmd_enableaccount +commands["enableaccount"] = cmd_enableaccount() +from samba.netcmd.newuser import cmd_newuser +commands["newuser"] = cmd_newuser() +from samba.netcmd.netacl import cmd_acl +commands["acl"] = cmd_acl() +from samba.netcmd.fsmo import cmd_fsmo +commands["fsmo"] = cmd_fsmo() +from samba.netcmd.export import cmd_export +commands["export"] = cmd_export() +from samba.netcmd.time import cmd_time +commands["time"] = cmd_time() +from samba.netcmd.user import cmd_user +commands["user"] = cmd_user() +from samba.netcmd.vampire import cmd_vampire +commands["vampire"] = cmd_vampire() +from samba.netcmd.machinepw import cmd_machinepw +commands["machinepw"] = cmd_machinepw() +from samba.netcmd.spn import cmd_spn +commands["spn"] = cmd_spn() +from samba.netcmd.group import cmd_group +commands["group"] = cmd_group() +from samba.netcmd.join import cmd_join +commands["join"] = cmd_join() +from samba.netcmd.rodc import cmd_rodc +commands["rodc"] = cmd_rodc() +from samba.netcmd.drs import cmd_drs +commands["drs"] = cmd_drs() +from samba.netcmd.gpo import cmd_gpo +commands["gpo2"] = cmd_gpo() +from samba.netcmd.ldapcmp import cmd_ldapcmp +commands["ldapcmp"] = cmd_ldapcmp() diff --git a/source4/scripting/python/samba/netcmd/common.py b/source4/scripting/python/samba/netcmd/common.py new file mode 100644 index 0000000000..bd72c8f361 --- /dev/null +++ b/source4/scripting/python/samba/netcmd/common.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python +# +# common functions for samba-tool python commands +# +# Copyright Andrew Tridgell 2010 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +def netcmd_dnsname(lp): + '''return the full DNS name of our own host. Used as a default + for hostname when running status queries''' + return lp.get('netbios name').lower() + "." + lp.get('realm').lower() diff --git a/source4/scripting/python/samba/netcmd/domainlevel.py b/source4/scripting/python/samba/netcmd/domainlevel.py new file mode 100644 index 0000000000..3d50ccea78 --- /dev/null +++ b/source4/scripting/python/samba/netcmd/domainlevel.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python +# +# Raises domain and forest function levels +# +# Copyright Matthias Dieter Wallnoefer 2009 +# Copyright Jelmer Vernooij 2009 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +# Notice: At the moment we have some more checks to do here on the special +# attributes (consider attribute "msDS-Behavior-Version). This is due to the +# fact that we on s4 LDB don't implement their change policy (only certain +# values, only increments possible...) yet. + +import samba.getopt as options +import ldb + +from samba.auth import system_session +from samba.netcmd import ( + Command, + CommandError, + Option, + ) +from samba.samdb import SamDB +from samba.dsdb import ( + DS_DOMAIN_FUNCTION_2000, + DS_DOMAIN_FUNCTION_2003, + DS_DOMAIN_FUNCTION_2003_MIXED, + DS_DOMAIN_FUNCTION_2008, + DS_DOMAIN_FUNCTION_2008_R2, + ) + +class cmd_domainlevel(Command): + """Raises domain and forest function levels""" + + synopsis = "(show | raise <options>)" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_options = [ + Option("-H", help="LDB URL for database or target server", type=str), + Option("--quiet", help="Be quiet", action="store_true"), + Option("--forest", type="choice", choices=["2003", "2008", "2008_R2"], + help="The forest function level (2003 | 2008 | 2008_R2)"), + Option("--domain", type="choice", choices=["2003", "2008", "2008_R2"], + help="The domain function level (2003 | 2008 | 2008_R2)"), + ] + + takes_args = ["subcommand"] + + def run(self, subcommand, H=None, forest=None, domain=None, quiet=False, + credopts=None, sambaopts=None, versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + + domain_dn = samdb.domain_dn() + + res_forest = samdb.search("CN=Partitions,CN=Configuration," + domain_dn, + scope=ldb.SCOPE_BASE, attrs=["msDS-Behavior-Version"]) + assert len(res_forest) == 1 + + res_domain = samdb.search(domain_dn, scope=ldb.SCOPE_BASE, + attrs=["msDS-Behavior-Version", "nTMixedDomain"]) + assert len(res_domain) == 1 + + res_dc_s = samdb.search("CN=Sites,CN=Configuration," + domain_dn, + scope=ldb.SCOPE_SUBTREE, expression="(objectClass=nTDSDSA)", + attrs=["msDS-Behavior-Version"]) + assert len(res_dc_s) >= 1 + + try: + level_forest = int(res_forest[0]["msDS-Behavior-Version"][0]) + level_domain = int(res_domain[0]["msDS-Behavior-Version"][0]) + level_domain_mixed = int(res_domain[0]["nTMixedDomain"][0]) + + min_level_dc = int(res_dc_s[0]["msDS-Behavior-Version"][0]) # Init value + for msg in res_dc_s: + if int(msg["msDS-Behavior-Version"][0]) < min_level_dc: + min_level_dc = int(msg["msDS-Behavior-Version"][0]) + + if level_forest < 0 or level_domain < 0: + raise CommandError("Domain and/or forest function level(s) is/are invalid. Correct them or reprovision!") + if min_level_dc < 0: + raise CommandError("Lowest function level of a DC is invalid. Correct this or reprovision!") + if level_forest > level_domain: + raise CommandError("Forest function level is higher than the domain level(s). Correct this or reprovision!") + if level_domain > min_level_dc: + raise CommandError("Domain function level is higher than the lowest function level of a DC. Correct this or reprovision!") + + except KeyError: + raise CommandError("Could not retrieve the actual domain, forest level and/or lowest DC function level!") + + if subcommand == "show": + self.message("Domain and forest function level for domain '%s'" % domain_dn) + if level_forest == DS_DOMAIN_FUNCTION_2000 and level_domain_mixed != 0: + self.message("\nATTENTION: You run SAMBA 4 on a forest function level lower than Windows 2000 (Native). This isn't supported! Please raise!") + if level_domain == DS_DOMAIN_FUNCTION_2000 and level_domain_mixed != 0: + self.message("\nATTENTION: You run SAMBA 4 on a domain function level lower than Windows 2000 (Native). This isn't supported! Please raise!") + if min_level_dc == DS_DOMAIN_FUNCTION_2000 and level_domain_mixed != 0: + self.message("\nATTENTION: You run SAMBA 4 on a lowest function level of a DC lower than Windows 2003. This isn't supported! Please step-up or upgrade the concerning DC(s)!") + + self.message("") + + if level_forest == DS_DOMAIN_FUNCTION_2000: + outstr = "2000" + elif level_forest == DS_DOMAIN_FUNCTION_2003_MIXED: + outstr = "2003 with mixed domains/interim (NT4 DC support)" + elif level_forest == DS_DOMAIN_FUNCTION_2003: + outstr = "2003" + elif level_forest == DS_DOMAIN_FUNCTION_2008: + outstr = "2008" + elif level_forest == DS_DOMAIN_FUNCTION_2008_R2: + outstr = "2008 R2" + else: + outstr = "higher than 2008 R2" + self.message("Forest function level: (Windows) " + outstr) + + if level_domain == DS_DOMAIN_FUNCTION_2000 and level_domain_mixed != 0: + outstr = "2000 mixed (NT4 DC support)" + elif level_domain == DS_DOMAIN_FUNCTION_2000 and level_domain_mixed == 0: + outstr = "2000" + elif level_domain == DS_DOMAIN_FUNCTION_2003_MIXED: + outstr = "2003 with mixed domains/interim (NT4 DC support)" + elif level_domain == DS_DOMAIN_FUNCTION_2003: + outstr = "2003" + elif level_domain == DS_DOMAIN_FUNCTION_2008: + outstr = "2008" + elif level_domain == DS_DOMAIN_FUNCTION_2008_R2: + outstr = "2008 R2" + else: + outstr = "higher than 2008 R2" + self.message("Domain function level: (Windows) " + outstr) + + if min_level_dc == DS_DOMAIN_FUNCTION_2000: + outstr = "2000" + elif min_level_dc == DS_DOMAIN_FUNCTION_2003: + outstr = "2003" + elif min_level_dc == DS_DOMAIN_FUNCTION_2008: + outstr = "2008" + elif min_level_dc == DS_DOMAIN_FUNCTION_2008_R2: + outstr = "2008 R2" + else: + outstr = "higher than 2008 R2" + self.message("Lowest function level of a DC: (Windows) " + outstr) + + elif subcommand == "raise": + msgs = [] + + if domain is not None: + if domain == "2003": + new_level_domain = DS_DOMAIN_FUNCTION_2003 + elif domain == "2008": + new_level_domain = DS_DOMAIN_FUNCTION_2008 + elif domain == "2008_R2": + new_level_domain = DS_DOMAIN_FUNCTION_2008_R2 + + if new_level_domain <= level_domain and level_domain_mixed == 0: + raise CommandError("Domain function level can't be smaller equal to the actual one!") + + if new_level_domain > min_level_dc: + raise CommandError("Domain function level can't be higher than the lowest function level of a DC!") + + # Deactivate mixed/interim domain support + if level_domain_mixed != 0: + # Directly on the base DN + m = ldb.Message() + m.dn = ldb.Dn(samdb, domain_dn) + m["nTMixedDomain"] = ldb.MessageElement("0", + ldb.FLAG_MOD_REPLACE, "nTMixedDomain") + samdb.modify(m) + # Under partitions + m = ldb.Message() + m.dn = ldb.Dn(samdb, "CN=" + lp.get("workgroup") + + ",CN=Partitions,CN=Configuration," + domain_dn) + m["nTMixedDomain"] = ldb.MessageElement("0", + ldb.FLAG_MOD_REPLACE, "nTMixedDomain") + try: + samdb.modify(m) + except ldb.LdbError, (enum, emsg): + if enum != ldb.ERR_UNWILLING_TO_PERFORM: + raise + + # Directly on the base DN + m = ldb.Message() + m.dn = ldb.Dn(samdb, domain_dn) + m["msDS-Behavior-Version"]= ldb.MessageElement( + str(new_level_domain), ldb.FLAG_MOD_REPLACE, + "msDS-Behavior-Version") + samdb.modify(m) + # Under partitions + m = ldb.Message() + m.dn = ldb.Dn(samdb, "CN=" + lp.get("workgroup") + + ",CN=Partitions,CN=Configuration," + domain_dn) + m["msDS-Behavior-Version"]= ldb.MessageElement( + str(new_level_domain), ldb.FLAG_MOD_REPLACE, + "msDS-Behavior-Version") + try: + samdb.modify(m) + except ldb.LdbError, (enum, emsg): + if enum != ldb.ERR_UNWILLING_TO_PERFORM: + raise + + level_domain = new_level_domain + msgs.append("Domain function level changed!") + + if forest is not None: + if forest == "2003": + new_level_forest = DS_DOMAIN_FUNCTION_2003 + elif forest == "2008": + new_level_forest = DS_DOMAIN_FUNCTION_2008 + elif forest == "2008_R2": + new_level_forest = DS_DOMAIN_FUNCTION_2008_R2 + if new_level_forest <= level_forest: + raise CommandError("Forest function level can't be smaller equal to the actual one!") + if new_level_forest > level_domain: + raise CommandError("Forest function level can't be higher than the domain function level(s). Please raise it/them first!") + m = ldb.Message() + m.dn = ldb.Dn(samdb, "CN=Partitions,CN=Configuration," + + domain_dn) + m["msDS-Behavior-Version"]= ldb.MessageElement( + str(new_level_forest), ldb.FLAG_MOD_REPLACE, + "msDS-Behavior-Version") + samdb.modify(m) + msgs.append("Forest function level changed!") + msgs.append("All changes applied successfully!") + self.message("\n".join(msgs)) + else: + raise CommandError("Wrong argument '%s'!" % subcommand) diff --git a/source4/scripting/python/samba/netcmd/drs.py b/source4/scripting/python/samba/netcmd/drs.py new file mode 100644 index 0000000000..7dea9de856 --- /dev/null +++ b/source4/scripting/python/samba/netcmd/drs.py @@ -0,0 +1,477 @@ +#!/usr/bin/env python +# +# implement samba_tool drs commands +# +# Copyright Andrew Tridgell 2010 +# +# based on C implementation by Kamen Mazdrashki <kamen.mazdrashki@postpath.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import samba.getopt as options +import ldb + +from samba.auth import system_session +from samba.netcmd import ( + Command, + CommandError, + Option, + SuperCommand, + ) +from samba.samdb import SamDB +from samba import drs_utils, nttime2string, dsdb +from samba.dcerpc import drsuapi, misc +import common + +def drsuapi_connect(ctx): + '''make a DRSUAPI connection to the server''' + binding_options = "seal" + if ctx.lp.get("log level") >= 5: + binding_options += ",print" + binding_string = "ncacn_ip_tcp:%s[%s]" % (ctx.server, binding_options) + try: + ctx.drsuapi = drsuapi.drsuapi(binding_string, ctx.lp, ctx.creds) + (ctx.drsuapi_handle, ctx.bind_supported_extensions) = drs_utils.drs_DsBind(ctx.drsuapi) + except Exception, e: + raise CommandError("DRS connection to %s failed" % ctx.server, e) + + +def samdb_connect(ctx): + '''make a ldap connection to the server''' + try: + ctx.samdb = SamDB(url="ldap://%s" % ctx.server, + session_info=system_session(), + credentials=ctx.creds, lp=ctx.lp) + except Exception, e: + raise CommandError("LDAP connection to %s failed" % ctx.server, e) + + +def drs_errmsg(werr): + '''return "was successful" or an error string''' + (ecode, estring) = werr + if ecode == 0: + return "was successful" + return "failed, result %u (%s)" % (ecode, estring) + + +def attr_default(msg, attrname, default): + '''get an attribute from a ldap msg with a default''' + if attrname in msg: + return msg[attrname][0] + return default + + +def drs_parse_ntds_dn(ntds_dn): + '''parse a NTDS DN returning a site and server''' + a = ntds_dn.split(',') + if a[0] != "CN=NTDS Settings" or a[2] != "CN=Servers" or a[4] != 'CN=Sites': + raise RuntimeError("bad NTDS DN %s" % ntds_dn) + server = a[1].split('=')[1] + site = a[3].split('=')[1] + return (site, server) + + +def get_dsServiceName(samdb): + '''get the NTDS DN from the rootDSE''' + res = samdb.search(base="", scope=ldb.SCOPE_BASE, attrs=["dsServiceName"]) + return res[0]["dsServiceName"][0] + + +class cmd_drs_showrepl(Command): + """show replication status""" + + synopsis = "%prog drs showrepl <DC>" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_args = ["DC?"] + + def print_neighbour(self, n): + '''print one set of neighbour information''' + self.message("%s" % n.naming_context_dn) + try: + (site, server) = drs_parse_ntds_dn(n.source_dsa_obj_dn) + self.message("\t%s\%s via RPC" % (site, server)) + except RuntimeError: + self.message("\tNTDS DN: %s" % n.source_dsa_obj_dn) + self.message("\t\tDSA object GUID: %s" % n.source_dsa_obj_guid) + self.message("\t\tLast attempt @ %s %s" % (nttime2string(n.last_attempt), + drs_errmsg(n.result_last_attempt))) + self.message("\t\t%u consecutive failure(s)." % n.consecutive_sync_failures) + self.message("\t\tLast success @ %s" % nttime2string(n.last_success)) + self.message("") + + def drsuapi_ReplicaInfo(ctx, info_type): + '''call a DsReplicaInfo''' + + req1 = drsuapi.DsReplicaGetInfoRequest1() + req1.info_type = info_type + try: + (info_type, info) = ctx.drsuapi.DsReplicaGetInfo(ctx.drsuapi_handle, 1, req1) + except Exception, e: + raise CommandError("DsReplicaGetInfo of type %u failed" % info_type, e) + return (info_type, info) + + + def run(self, DC=None, sambaopts=None, + credopts=None, versionopts=None, server=None): + + self.lp = sambaopts.get_loadparm() + if DC is None: + DC = common.netcmd_dnsname(self.lp) + self.server = DC + self.creds = credopts.get_credentials(self.lp, fallback_machine=True) + + drsuapi_connect(self) + samdb_connect(self) + + # show domain information + ntds_dn = get_dsServiceName(self.samdb) + server_dns = self.samdb.search(base="", scope=ldb.SCOPE_BASE, attrs=["dnsHostName"])[0]['dnsHostName'][0] + + (site, server) = drs_parse_ntds_dn(ntds_dn) + try: + ntds = self.samdb.search(base=ntds_dn, scope=ldb.SCOPE_BASE, attrs=['options', 'objectGUID', 'invocationId']) + except Exception, e: + raise CommandError("Failed to search NTDS DN %s" % ntds_dn) + conn = self.samdb.search(base=ntds_dn, expression="(objectClass=nTDSConnection)") + + self.message("%s\\%s" % (site, server)) + self.message("DSA Options: 0x%08x" % int(attr_default(ntds[0], "options", 0))) + self.message("DSA object GUID: %s" % self.samdb.schema_format_value("objectGUID", ntds[0]["objectGUID"][0])) + self.message("DSA invocationId: %s\n" % self.samdb.schema_format_value("objectGUID", ntds[0]["invocationId"][0])) + + self.message("==== INBOUND NEIGHBORS ====\n") + (info_type, info) = self.drsuapi_ReplicaInfo(drsuapi.DRSUAPI_DS_REPLICA_INFO_NEIGHBORS) + for n in info.array: + self.print_neighbour(n) + + + self.message("==== OUTBOUND NEIGHBORS ====\n") + (info_type, info) = self.drsuapi_ReplicaInfo(drsuapi.DRSUAPI_DS_REPLICA_INFO_REPSTO) + for n in info.array: + self.print_neighbour(n) + + reasons = ['NTDSCONN_KCC_GC_TOPOLOGY', + 'NTDSCONN_KCC_RING_TOPOLOGY', + 'NTDSCONN_KCC_MINIMIZE_HOPS_TOPOLOGY', + 'NTDSCONN_KCC_STALE_SERVERS_TOPOLOGY', + 'NTDSCONN_KCC_OSCILLATING_CONNECTION_TOPOLOGY', + 'NTDSCONN_KCC_INTERSITE_GC_TOPOLOGY', + 'NTDSCONN_KCC_INTERSITE_TOPOLOGY', + 'NTDSCONN_KCC_SERVER_FAILOVER_TOPOLOGY', + 'NTDSCONN_KCC_SITE_FAILOVER_TOPOLOGY', + 'NTDSCONN_KCC_REDUNDANT_SERVER_TOPOLOGY'] + + self.message("==== KCC CONNECTION OBJECTS ====\n") + for c in conn: + self.message("Connection --") + self.message("\tConnection name: %s" % c['name'][0]) + self.message("\tEnabled : %s" % attr_default(c, 'enabledConnection', 'TRUE')) + self.message("\tServer DNS name : %s" % server_dns) + self.message("\tServer DN name : %s" % c['fromServer'][0]) + self.message("\t\tTransportType: RPC") + self.message("\t\toptions: 0x%08X" % int(attr_default(c, 'options', 0))) + if not 'mS-DS-ReplicatesNCReason' in c: + self.message("Warning: No NC replicated for Connection!") + continue + for r in c['mS-DS-ReplicatesNCReason']: + a = str(r).split(':') + self.message("\t\tReplicatesNC: %s" % a[3]) + self.message("\t\tReason: 0x%08x" % int(a[2])) + for s in reasons: + if getattr(dsdb, s, 0) & int(a[2]): + self.message("\t\t\t%s" % s) + + +class cmd_drs_kcc(Command): + """trigger knowledge consistency center run""" + + synopsis = "%prog drs kcc <DC>" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_args = ["DC?"] + + def run(self, DC=None, sambaopts=None, + credopts=None, versionopts=None, server=None): + + self.lp = sambaopts.get_loadparm() + if DC is None: + DC = common.netcmd_dnsname(self.lp) + self.server = DC + + self.creds = credopts.get_credentials(self.lp, fallback_machine=True) + + drsuapi_connect(self) + + req1 = drsuapi.DsExecuteKCC1() + try: + self.drsuapi.DsExecuteKCC(self.drsuapi_handle, 1, req1) + except Exception, e: + raise CommandError("DsExecuteKCC failed", e) + self.message("Consistency check on %s successful." % DC) + + + +class cmd_drs_replicate(Command): + """replicate a naming context between two DCs""" + + synopsis = "%prog drs replicate <DEST_DC> <SOURCE_DC> <NC>" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_args = ["DEST_DC", "SOURCE_DC", "NC"] + + takes_options = [ + Option("--add-ref", help="use ADD_REF to add to repsTo on source", action="store_true"), + Option("--sync-forced", help="use SYNC_FORCED to force inbound replication", action="store_true"), + ] + + def run(self, DEST_DC, SOURCE_DC, NC, add_ref=False, sync_forced=False, + sambaopts=None, + credopts=None, versionopts=None, server=None): + + self.server = DEST_DC + self.lp = sambaopts.get_loadparm() + + self.creds = credopts.get_credentials(self.lp, fallback_machine=True) + + drsuapi_connect(self) + samdb_connect(self) + + # we need to find the NTDS GUID of the source DC + msg = self.samdb.search(base=self.samdb.get_config_basedn(), + expression="(&(objectCategory=server)(|(name=%s)(dNSHostName=%s)))" % (SOURCE_DC, + SOURCE_DC), + attrs=[]) + if len(msg) == 0: + raise CommandError("Failed to find source DC %s" % SOURCE_DC) + server_dn = msg[0]['dn'] + + msg = self.samdb.search(base=server_dn, scope=ldb.SCOPE_ONELEVEL, + expression="(|(objectCategory=nTDSDSA)(objectCategory=nTDSDSARO))", + attrs=['objectGUID', 'options']) + if len(msg) == 0: + raise CommandError("Failed to find source NTDS DN %s" % SOURCE_DC) + source_dsa_guid = msg[0]['objectGUID'][0] + options = int(attr_default(msg, 'options', 0)) + + nc = drsuapi.DsReplicaObjectIdentifier() + nc.dn = NC + + req1 = drsuapi.DsReplicaSyncRequest1() + req1.naming_context = nc; + req1.options = 0 + if not (options & dsdb.DS_NTDSDSA_OPT_DISABLE_OUTBOUND_REPL): + req1.options |= drsuapi.DRSUAPI_DRS_WRIT_REP + if add_ref: + req1.options |= drsuapi.DRSUAPI_DRS_ADD_REF + if sync_forced: + req1.options |= drsuapi.DRSUAPI_DRS_SYNC_FORCED + req1.source_dsa_guid = misc.GUID(source_dsa_guid) + + try: + self.drsuapi.DsReplicaSync(self.drsuapi_handle, 1, req1) + except Exception, estr: + raise CommandError("DsReplicaSync failed", estr) + self.message("Replicate from %s to %s was successful." % (SOURCE_DC, DEST_DC)) + + + +class cmd_drs_bind(Command): + """show DRS capabilities of a server""" + + synopsis = "%prog drs bind <DC>" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_args = ["DC?"] + + def run(self, DC=None, sambaopts=None, + credopts=None, versionopts=None, server=None): + + self.lp = sambaopts.get_loadparm() + if DC is None: + DC = common.netcmd_dnsname(self.lp) + self.server = DC + self.creds = credopts.get_credentials(self.lp, fallback_machine=True) + + drsuapi_connect(self) + samdb_connect(self) + + bind_info = drsuapi.DsBindInfoCtr() + bind_info.length = 28 + bind_info.info = drsuapi.DsBindInfo28() + (info, handle) = self.drsuapi.DsBind(misc.GUID(drsuapi.DRSUAPI_DS_BIND_GUID), bind_info) + + optmap = [ + ("DRSUAPI_SUPPORTED_EXTENSION_BASE" , "DRS_EXT_BASE"), + ("DRSUAPI_SUPPORTED_EXTENSION_ASYNC_REPLICATION" , "DRS_EXT_ASYNCREPL"), + ("DRSUAPI_SUPPORTED_EXTENSION_REMOVEAPI" , "DRS_EXT_REMOVEAPI"), + ("DRSUAPI_SUPPORTED_EXTENSION_MOVEREQ_V2" , "DRS_EXT_MOVEREQ_V2"), + ("DRSUAPI_SUPPORTED_EXTENSION_GETCHG_COMPRESS" , "DRS_EXT_GETCHG_DEFLATE"), + ("DRSUAPI_SUPPORTED_EXTENSION_DCINFO_V1" , "DRS_EXT_DCINFO_V1"), + ("DRSUAPI_SUPPORTED_EXTENSION_RESTORE_USN_OPTIMIZATION" , "DRS_EXT_RESTORE_USN_OPTIMIZATION"), + ("DRSUAPI_SUPPORTED_EXTENSION_ADDENTRY" , "DRS_EXT_ADDENTRY"), + ("DRSUAPI_SUPPORTED_EXTENSION_KCC_EXECUTE" , "DRS_EXT_KCC_EXECUTE"), + ("DRSUAPI_SUPPORTED_EXTENSION_ADDENTRY_V2" , "DRS_EXT_ADDENTRY_V2"), + ("DRSUAPI_SUPPORTED_EXTENSION_LINKED_VALUE_REPLICATION" , "DRS_EXT_LINKED_VALUE_REPLICATION"), + ("DRSUAPI_SUPPORTED_EXTENSION_DCINFO_V2" , "DRS_EXT_DCINFO_V2"), + ("DRSUAPI_SUPPORTED_EXTENSION_INSTANCE_TYPE_NOT_REQ_ON_MOD","DRS_EXT_INSTANCE_TYPE_NOT_REQ_ON_MOD"), + ("DRSUAPI_SUPPORTED_EXTENSION_CRYPTO_BIND" , "DRS_EXT_CRYPTO_BIND"), + ("DRSUAPI_SUPPORTED_EXTENSION_GET_REPL_INFO" , "DRS_EXT_GET_REPL_INFO"), + ("DRSUAPI_SUPPORTED_EXTENSION_STRONG_ENCRYPTION" , "DRS_EXT_STRONG_ENCRYPTION"), + ("DRSUAPI_SUPPORTED_EXTENSION_DCINFO_V01" , "DRS_EXT_DCINFO_VFFFFFFFF"), + ("DRSUAPI_SUPPORTED_EXTENSION_TRANSITIVE_MEMBERSHIP" , "DRS_EXT_TRANSITIVE_MEMBERSHIP"), + ("DRSUAPI_SUPPORTED_EXTENSION_ADD_SID_HISTORY" , "DRS_EXT_ADD_SID_HISTORY"), + ("DRSUAPI_SUPPORTED_EXTENSION_POST_BETA3" , "DRS_EXT_POST_BETA3"), + ("DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V5" , "DRS_EXT_GETCHGREQ_V5"), + ("DRSUAPI_SUPPORTED_EXTENSION_GET_MEMBERSHIPS2" , "DRS_EXT_GETMEMBERSHIPS2"), + ("DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V6" , "DRS_EXT_GETCHGREQ_V6"), + ("DRSUAPI_SUPPORTED_EXTENSION_NONDOMAIN_NCS" , "DRS_EXT_NONDOMAIN_NCS"), + ("DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V8" , "DRS_EXT_GETCHGREQ_V8"), + ("DRSUAPI_SUPPORTED_EXTENSION_GETCHGREPLY_V5" , "DRS_EXT_GETCHGREPLY_V5"), + ("DRSUAPI_SUPPORTED_EXTENSION_GETCHGREPLY_V6" , "DRS_EXT_GETCHGREPLY_V6"), + ("DRSUAPI_SUPPORTED_EXTENSION_ADDENTRYREPLY_V3" , "DRS_EXT_WHISTLER_BETA3"), + ("DRSUAPI_SUPPORTED_EXTENSION_GETCHGREPLY_V7" , "DRS_EXT_WHISTLER_BETA3"), + ("DRSUAPI_SUPPORTED_EXTENSION_VERIFY_OBJECT" , "DRS_EXT_WHISTLER_BETA3"), + ("DRSUAPI_SUPPORTED_EXTENSION_XPRESS_COMPRESS" , "DRS_EXT_W2K3_DEFLATE"), + ("DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V10" , "DRS_EXT_GETCHGREQ_V10"), + ("DRSUAPI_SUPPORTED_EXTENSION_RESERVED_PART2" , "DRS_EXT_RESERVED_FOR_WIN2K_OR_DOTNET_PART2"), + ("DRSUAPI_SUPPORTED_EXTENSION_RESERVED_PART3" , "DRS_EXT_RESERVED_FOR_WIN2K_OR_DOTNET_PART3") + ] + + optmap_ext = [ + ("DRSUAPI_SUPPORTED_EXTENSION_ADAM", "DRS_EXT_ADAM"), + ("DRSUAPI_SUPPORTED_EXTENSION_LH_BETA2", "DRS_EXT_LH_BETA2"), + ("DRSUAPI_SUPPORTED_EXTENSION_RECYCLE_BIN", "DRS_EXT_RECYCLE_BIN")] + + self.message("Bind to %s succeeded." % DC) + self.message("Extensions supported:") + for (opt, str) in optmap: + optval = getattr(drsuapi, opt, 0) + if info.info.supported_extensions & optval: + yesno = "Yes" + else: + yesno = "No " + self.message(" %-60s: %s (%s)" % (opt, yesno, str)) + + if isinstance(info.info, drsuapi.DsBindInfo48): + self.message("\nExtended Extensions supported:") + for (opt, str) in optmap_ext: + optval = getattr(drsuapi, opt, 0) + if info.info.supported_extensions_ext & optval: + yesno = "Yes" + else: + yesno = "No " + self.message(" %-60s: %s (%s)" % (opt, yesno, str)) + + self.message("\nSite GUID: %s" % info.info.site_guid) + self.message("Repl epoch: %u" % info.info.repl_epoch) + if isinstance(info.info, drsuapi.DsBindInfo48): + self.message("Forest GUID: %s" % info.info.config_dn_guid) + + + +class cmd_drs_options(Command): + """query or change 'options' for NTDS Settings object of a domain controller""" + + synopsis = ("%prog drs options <DC>" + " [--dsa-option={+|-}IS_GC | {+|-}DISABLE_INBOUND_REPL" + " |{+|-}DISABLE_OUTBOUND_REPL | {+|-}DISABLE_NTDSCONN_XLATE]") + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_args = ["DC"] + + takes_options = [ + Option("--dsa-option", help="DSA option to enable/disable", type="str"), + ] + + option_map = {"IS_GC": 0x00000001, + "DISABLE_INBOUND_REPL": 0x00000002, + "DISABLE_OUTBOUND_REPL": 0x00000004, + "DISABLE_NTDSCONN_XLATE": 0x00000008} + + def run(self, DC, dsa_option=None, + sambaopts=None, credopts=None, versionopts=None): + + self.lp = sambaopts.get_loadparm() + if DC is None: + DC = common.netcmd_dnsname(self.lp) + self.server = DC + self.creds = credopts.get_credentials(self.lp, fallback_machine=True) + + samdb_connect(self) + + ntds_dn = get_dsServiceName(self.samdb) + res = self.samdb.search(base=ntds_dn, scope=ldb.SCOPE_BASE, attrs=["options"]) + dsa_opts = int(res[0]["options"][0]) + + # print out current DSA options + cur_opts = [x for x in self.option_map if self.option_map[x] & dsa_opts] + self.message("Current DSA options: " + ", ".join(cur_opts)) + + # modify options + if dsa_option: + if dsa_option[:1] not in ("+", "-"): + raise CommandError("Unknown option %s" % dsa_option) + flag = dsa_option[1:] + if flag not in self.option_map.keys(): + raise CommandError("Unknown option %s" % dsa_option) + if dsa_option[:1] == "+": + dsa_opts |= self.option_map[flag] + else: + dsa_opts &= ~self.option_map[flag] + #save new options + m = ldb.Message() + m.dn = ldb.Dn(self.samdb, ntds_dn) + m["options"]= ldb.MessageElement(str(dsa_opts), ldb.FLAG_MOD_REPLACE, "options") + self.samdb.modify(m) + # print out new DSA options + cur_opts = [x for x in self.option_map if self.option_map[x] & dsa_opts] + self.message("New DSA options: " + ", ".join(cur_opts)) + + +class cmd_drs(SuperCommand): + """DRS commands""" + + subcommands = {} + subcommands["bind"] = cmd_drs_bind() + subcommands["kcc"] = cmd_drs_kcc() + subcommands["replicate"] = cmd_drs_replicate() + subcommands["showrepl"] = cmd_drs_showrepl() + subcommands["options"] = cmd_drs_options() diff --git a/source4/scripting/python/samba/netcmd/dsacl.py b/source4/scripting/python/samba/netcmd/dsacl.py new file mode 100644 index 0000000000..58a3552687 --- /dev/null +++ b/source4/scripting/python/samba/netcmd/dsacl.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python +# +# Manipulate ACLs on directory objects +# +# Copyright (C) Nadezhda Ivanova <nivanova@samba.org> 2010 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import samba.getopt as options +from samba.dcerpc import security +from samba.samdb import SamDB +from samba.ndr import ndr_unpack, ndr_pack +from samba.dcerpc.security import ( + GUID_DRS_ALLOCATE_RIDS, GUID_DRS_CHANGE_DOMAIN_MASTER, + GUID_DRS_CHANGE_INFR_MASTER, GUID_DRS_CHANGE_PDC, + GUID_DRS_CHANGE_RID_MASTER, GUID_DRS_CHANGE_SCHEMA_MASTER, + GUID_DRS_GET_CHANGES, GUID_DRS_GET_ALL_CHANGES, + GUID_DRS_GET_FILTERED_ATTRIBUTES, GUID_DRS_MANAGE_TOPOLOGY, + GUID_DRS_MONITOR_TOPOLOGY, GUID_DRS_REPL_SYNCRONIZE, + GUID_DRS_RO_REPL_SECRET_SYNC) + + +import ldb +from ldb import SCOPE_BASE +import re + +from samba.auth import system_session +from samba.netcmd import ( + Command, + CommandError, + SuperCommand, + Option, + ) + +class cmd_ds_acl_set(Command): + """Modify access list on a directory object""" + + synopsis = "set --objectdn=objectdn --car=control right --action=[deny|allow] --trusteedn=trustee-dn" + car_help = """ The access control right to allow or deny """ + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_options = [ + Option("--host", help="LDB URL for database or target server", + type=str), + Option("--car", type="choice", choices=["change-rid", + "change-pdc", + "change-infrastructure", + "change-schema", + "change-naming", + "allocate_rids", + "get-changes", + "get-changes-all", + "get-changes-filtered", + "topology-manage", + "topology-monitor", + "repl-sync", + "ro-repl-secret-sync"], + help=car_help), + Option("--action", type="choice", choices=["allow", "deny"], + help="""Deny or allow access"""), + Option("--objectdn", help="DN of the object whose SD to modify", + type="string"), + Option("--trusteedn", help="DN of the entity that gets access", + type="string"), + Option("--sddl", help="An ACE or group of ACEs to be added on the object", + type="string"), + ] + + def find_trustee_sid(self, samdb, trusteedn): + res = samdb.search(base=trusteedn, expression="(objectClass=*)", + scope=SCOPE_BASE) + assert(len(res) == 1) + return ndr_unpack( security.dom_sid,res[0]["objectSid"][0]) + + def modify_descriptor(self, samdb, object_dn, desc, controls=None): + assert(isinstance(desc, security.descriptor)) + m = ldb.Message() + m.dn = ldb.Dn(samdb, object_dn) + m["nTSecurityDescriptor"]= ldb.MessageElement( + (ndr_pack(desc)), ldb.FLAG_MOD_REPLACE, + "nTSecurityDescriptor") + samdb.modify(m) + + def read_descriptor(self, samdb, object_dn): + res = samdb.search(base=object_dn, scope=SCOPE_BASE, + attrs=["nTSecurityDescriptor"]) + # we should theoretically always have an SD + assert(len(res) == 1) + desc = res[0]["nTSecurityDescriptor"][0] + return ndr_unpack(security.descriptor, desc) + + def get_domain_sid(self, samdb): + res = samdb.search(base=samdb.domain_dn(), + expression="(objectClass=*)", scope=SCOPE_BASE) + return ndr_unpack( security.dom_sid,res[0]["objectSid"][0]) + + def add_ace(self, samdb, object_dn, new_ace): + """Add new ace explicitly.""" + desc = self.read_descriptor(samdb, object_dn) + desc_sddl = desc.as_sddl(self.get_domain_sid(samdb)) + #TODO add bindings for descriptor manipulation and get rid of this + desc_aces = re.findall("\(.*?\)", desc_sddl) + for ace in desc_aces: + if ("ID" in ace): + desc_sddl = desc_sddl.replace(ace, "") + if new_ace in desc_sddl: + return + if desc_sddl.find("(") >= 0: + desc_sddl = desc_sddl[:desc_sddl.index("(")] + new_ace + desc_sddl[desc_sddl.index("("):] + else: + desc_sddl = desc_sddl + new_ace + desc = security.descriptor.from_sddl(desc_sddl, self.get_domain_sid(samdb)) + self.modify_descriptor(samdb, object_dn, desc) + + def print_new_acl(self, samdb, object_dn): + desc = self.read_descriptor(samdb, object_dn) + desc_sddl = desc.as_sddl(self.get_domain_sid(samdb)) + print "new descriptor for %s:" % object_dn + print desc_sddl + + def run(self, car, action, objectdn, trusteedn, sddl, + host=None, credopts=None, sambaopts=None, versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + + if sddl is None and (car is None or action is None + or objectdn is None or trusteedn is None): + return self.usage() + + samdb = SamDB(url=host, session_info=system_session(), + credentials=creds, lp=lp) + cars = {'change-rid' : GUID_DRS_CHANGE_RID_MASTER, + 'change-pdc' : GUID_DRS_CHANGE_PDC, + 'change-infrastructure' : GUID_DRS_CHANGE_INFR_MASTER, + 'change-schema' : GUID_DRS_CHANGE_SCHEMA_MASTER, + 'change-naming' : GUID_DRS_CHANGE_DOMAIN_MASTER, + 'allocate_rids' : GUID_DRS_ALLOCATE_RIDS, + 'get-changes' : GUID_DRS_GET_CHANGES, + 'get-changes-all' : GUID_DRS_GET_ALL_CHANGES, + 'get-changes-filtered' : GUID_DRS_GET_FILTERED_ATTRIBUTES, + 'topology-manage' : GUID_DRS_MANAGE_TOPOLOGY, + 'topology-monitor' : GUID_DRS_MONITOR_TOPOLOGY, + 'repl-sync' : GUID_DRS_REPL_SYNCRONIZE, + 'ro-repl-secret-sync' : GUID_DRS_RO_REPL_SECRET_SYNC, + } + sid = self.find_trustee_sid(samdb, trusteedn) + if sddl: + new_ace = sddl + elif action == "allow": + new_ace = "(OA;;CR;%s;;%s)" % (cars[car], str(sid)) + elif action == "deny": + new_ace = "(OD;;CR;%s;;%s)" % (cars[car], str(sid)) + else: + raise CommandError("Wrong argument '%s'!" % action) + + self.print_new_acl(samdb, objectdn) + self.add_ace(samdb, objectdn, new_ace) + self.print_new_acl(samdb, objectdn) + + +class cmd_ds_acl(SuperCommand): + """DS ACLs manipulation""" + + subcommands = {} + subcommands["set"] = cmd_ds_acl_set() diff --git a/source4/scripting/python/samba/netcmd/enableaccount.py b/source4/scripting/python/samba/netcmd/enableaccount.py new file mode 100644 index 0000000000..3ceddb3fd9 --- /dev/null +++ b/source4/scripting/python/samba/netcmd/enableaccount.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# +# Enables an user account on a Samba4 server +# Copyright Jelmer Vernooij 2008 +# +# Based on the original in EJS: +# Copyright Andrew Tridgell 2005 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import samba.getopt as options + +from samba.auth import system_session +from samba.netcmd import Command, CommandError, Option +from samba.samdb import SamDB + +class cmd_enableaccount(Command): + """Enables a user""" + + synopsis = "enableaccount [username] [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", help="LDB URL for database or target server", type=str), + Option("--filter", help="LDAP Filter to set password on", type=str), + ] + + takes_args = ["username?"] + + def run(self, username=None, sambaopts=None, credopts=None, + versionopts=None, filter=None, H=None): + if username is None and filter is None: + raise CommandError("Either the username or '--filter' must be specified!") + + if filter is None: + filter = "(&(objectClass=user)(sAMAccountName=%s))" % (username) + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + samdb.enable_account(filter) diff --git a/source4/scripting/python/samba/netcmd/export.py b/source4/scripting/python/samba/netcmd/export.py new file mode 100644 index 0000000000..649a2621b1 --- /dev/null +++ b/source4/scripting/python/samba/netcmd/export.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +# +# Export keytab +# +# Copyright Jelmer Vernooij 2010 <jelmer@samba.org> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import samba.getopt as options + +from samba.net import Net + +from samba.netcmd import ( + Command, + SuperCommand, + ) + +class cmd_export_keytab(Command): + """Dumps kerberos keys of the domain into a keytab""" + synopsis = "%prog export keytab <keytab>" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_options = [ + ] + + takes_args = ["keytab"] + + def run(self, keytab, credopts=None, sambaopts=None, versionopts=None): + lp = sambaopts.get_loadparm() + net = Net(None, lp, server=credopts.ipaddress) + net.export_keytab(keytab=keytab) + + +class cmd_export(SuperCommand): + """Dumps the sam of the domain we are joined to [server connection needed]""" + + subcommands = {} + subcommands["keytab"] = cmd_export_keytab() + diff --git a/source4/scripting/python/samba/netcmd/fsmo.py b/source4/scripting/python/samba/netcmd/fsmo.py new file mode 100644 index 0000000000..0c8d17c74f --- /dev/null +++ b/source4/scripting/python/samba/netcmd/fsmo.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python +# +# Changes a FSMO role owner +# +# Copyright Nadezhda Ivanova 2009 +# Copyright Jelmer Vernooij 2009 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import samba.getopt as options +import ldb +from ldb import LdbError + +from samba.auth import system_session +from samba.netcmd import ( + Command, + CommandError, + Option, + ) +from samba.samdb import SamDB + +class cmd_fsmo(Command): + """Makes the targer DC transfer or seize a fsmo role [server connection needed]""" + + synopsis = "(show | transfer <options> | seize <options>)" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_options = [ + Option("--host", help="LDB URL for database or target server", type=str), + Option("--force", help="Force seizing of the role without attempting to transfer first.", action="store_true"), + Option("--role", type="choice", choices=["rid", "pdc", "infrastructure","schema","naming","all"], + help="""The FSMO role to seize or transfer.\n +rid=RidAllocationMasterRole\n +schema=SchemaMasterRole\n +pdc=PdcEmulationMasterRole\n +naming=DomainNamingMasterRole\n +infrastructure=InfrastructureMasterRole\n +all=all of the above"""), + ] + + takes_args = ["subcommand"] + + def transfer_role(self, role, samdb): + m = ldb.Message() + m.dn = ldb.Dn(samdb, "") + if role == "rid": + m["becomeRidMaster"]= ldb.MessageElement( + "1", ldb.FLAG_MOD_REPLACE, + "becomeRidMaster") + elif role == "pdc": + domain_dn = samdb.domain_dn() + res = samdb.search(domain_dn, + scope=ldb.SCOPE_BASE, attrs=["objectSid"]) + assert len(res) == 1 + sid = res[0]["objectSid"][0] + m["becomePdc"]= ldb.MessageElement( + sid, ldb.FLAG_MOD_REPLACE, + "becomePdc") + elif role == "naming": + m["becomeDomainMaster"]= ldb.MessageElement( + "1", ldb.FLAG_MOD_REPLACE, + "becomeDomainMaster") + samdb.modify(m) + elif role == "infrastructure": + m["becomeInfrastructureMaster"]= ldb.MessageElement( + "1", ldb.FLAG_MOD_REPLACE, + "becomeInfrastructureMaster") + elif role == "schema": + m["becomeSchemaMaster"]= ldb.MessageElement( + "1", ldb.FLAG_MOD_REPLACE, + "becomeSchemaMaster") + else: + raise CommandError("Invalid FSMO role.") + samdb.modify(m) + + def seize_role(self, role, samdb, force): + res = samdb.search("", + scope=ldb.SCOPE_BASE, attrs=["dsServiceName"]) + assert len(res) == 1 + serviceName = res[0]["dsServiceName"][0] + domain_dn = samdb.domain_dn() + m = ldb.Message() + if role == "rid": + m.dn = ldb.Dn(samdb, self.rid_dn) + elif role == "pdc": + m.dn = ldb.Dn(samdb, domain_dn) + elif role == "naming": + m.dn = ldb.Dn(samdb, self.naming_dn) + elif role == "infrastructure": + m.dn = ldb.Dn(samdb, self.infrastructure_dn) + elif role == "schema": + m.dn = ldb.Dn(samdb, self.schema_dn) + else: + raise CommandError("Invalid FSMO role.") + #first try to transfer to avoid problem if the owner is still active + if force is None: + self.message("Attempting transfer...") + try: + self.transfer_role(role, samdb) + except LdbError, (num, _): + #transfer failed, use the big axe... + self.message("Transfer unsuccessfull, seizing...") + m["fSMORoleOwner"]= ldb.MessageElement( + serviceName, ldb.FLAG_MOD_REPLACE, + "fSMORoleOwner") + samdb.modify(m) + else: + self.message("Transfer succeeded.") + else: + self.message("Will not attempt transfer, seizing...") + m["fSMORoleOwner"]= ldb.MessageElement( + serviceName, ldb.FLAG_MOD_REPLACE, + "fSMORoleOwner") + samdb.modify(m) + + def run(self, subcommand, force=None, host=None, role=None, + credopts=None, sambaopts=None, versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + + samdb = SamDB(url=host, session_info=system_session(), + credentials=creds, lp=lp) + + domain_dn = samdb.domain_dn() + self.infrastructure_dn = "CN=Infrastructure," + domain_dn + self.naming_dn = "CN=Partitions,CN=Configuration," + domain_dn + self.schema_dn = "CN=Schema,CN=Configuration," + domain_dn + self.rid_dn = "CN=RID Manager$,CN=System," + domain_dn + + res = samdb.search(self.infrastructure_dn, + scope=ldb.SCOPE_BASE, attrs=["fSMORoleOwner"]) + assert len(res) == 1 + self.infrastructureMaster = res[0]["fSMORoleOwner"][0] + + res = samdb.search(domain_dn, + scope=ldb.SCOPE_BASE, attrs=["fSMORoleOwner"]) + assert len(res) == 1 + self.pdcEmulator = res[0]["fSMORoleOwner"][0] + + res = samdb.search(self.naming_dn, + scope=ldb.SCOPE_BASE, attrs=["fSMORoleOwner"]) + assert len(res) == 1 + self.namingMaster = res[0]["fSMORoleOwner"][0] + + res = samdb.search(self.schema_dn, + scope=ldb.SCOPE_BASE, attrs=["fSMORoleOwner"]) + assert len(res) == 1 + self.schemaMaster = res[0]["fSMORoleOwner"][0] + + res = samdb.search(self.rid_dn, + scope=ldb.SCOPE_BASE, attrs=["fSMORoleOwner"]) + assert len(res) == 1 + self.ridMaster = res[0]["fSMORoleOwner"][0] + + if subcommand == "show": + self.message("InfrastructureMasterRole owner: " + self.infrastructureMaster) + self.message("RidAllocationMasterRole owner: " + self.ridMaster) + self.message("PdcEmulationMasterRole owner: " + self.pdcEmulator) + self.message("DomainNamingMasterRole owner: " + self.namingMaster) + self.message("SchemaMasterRole owner: " + self.schemaMaster) + elif subcommand == "transfer": + if role == "all": + self.transfer_role("rid", samdb) + self.transfer_role("pdc", samdb) + self.transfer_role("naming", samdb) + self.transfer_role("infrastructure", samdb) + self.transfer_role("schema", samdb) + else: + self.transfer_role(role, samdb) + elif subcommand == "seize": + if role == "all": + self.seize_role("rid", samdb, force) + self.seize_role("pdc", samdb, force) + self.seize_role("naming", samdb, force) + self.seize_role("infrastructure", samdb, force) + self.seize_role("schema", samdb, force) + else: + self.seize_role(role, samdb, force) + else: + raise CommandError("Wrong argument '%s'!" % subcommand) diff --git a/source4/scripting/python/samba/netcmd/gpo.py b/source4/scripting/python/samba/netcmd/gpo.py new file mode 100644 index 0000000000..19007b361c --- /dev/null +++ b/source4/scripting/python/samba/netcmd/gpo.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python +# +# implement samba_tool gpo commands +# +# Copyright Andrew Tridgell 2010 +# +# based on C implementation by Guenther Deschner and Wilco Baan Hofman +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import samba.getopt as options +import ldb + +from samba.auth import system_session +from samba.netcmd import ( + Command, + CommandError, + Option, + SuperCommand, + ) +from samba.samdb import SamDB +from samba import drs_utils, nttime2string, dsdb, dcerpc +from samba.dcerpc import misc +from samba.ndr import ndr_unpack +import samba.security +import samba.auth +from samba.auth import AUTH_SESSION_INFO_DEFAULT_GROUPS, AUTH_SESSION_INFO_AUTHENTICATED, AUTH_SESSION_INFO_SIMPLE_PRIVILEGES + +def samdb_connect(ctx): + '''make a ldap connection to the server''' + try: + ctx.samdb = SamDB(url=ctx.url, + session_info=system_session(), + credentials=ctx.creds, lp=ctx.lp) + except Exception, e: + raise CommandError("LDAP connection to %s failed " % ctx.url, e) + + +def attr_default(msg, attrname, default): + '''get an attribute from a ldap msg with a default''' + if attrname in msg: + return msg[attrname][0] + return default + + +def flags_string(flags, value): + '''return a set of flags as a string''' + if value == 0: + return 'NONE' + ret = '' + for (str, val) in flags: + if val & value: + ret += str + ' ' + value &= ~val + if value != 0: + ret += '0x%08x' % value + return ret.rstrip() + + +def parse_gplink(gplink): + '''parse a gPLink into an array of dn and options''' + ret = [] + a = gplink.split(']') + for g in a: + if not g: + continue + d = g.split(';') + if len(d) != 2 or not d[0].startswith("[LDAP://"): + raise RuntimeError("Badly formed gPLink '%s'" % g) + ret.append({ 'dn' : d[0][8:], 'options' : int(d[1])}) + return ret + + +class cmd_listall(Command): + """list all GPOs""" + + synopsis = "%prog gpo listall" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", help="LDB URL for database or target server", type=str) + ] + + def run(self, H=None, sambaopts=None, + credopts=None, versionopts=None, server=None): + + self.url = H + self.lp = sambaopts.get_loadparm() + + self.creds = credopts.get_credentials(self.lp, fallback_machine=True) + + samdb_connect(self) + + policies_dn = self.samdb.get_default_basedn() + policies_dn.add_child(ldb.Dn(self.samdb, "CN=Policies,CN=System")) + + gpo_flags = [ + ("GPO_FLAG_USER_DISABLE", dsdb.GPO_FLAG_USER_DISABLE ), + ( "GPO_FLAG_MACHINE_DISABLE", dsdb.GPO_FLAG_MACHINE_DISABLE ) ] + + try: + msg = self.samdb.search(base=policies_dn, scope=ldb.SCOPE_ONELEVEL, + expression="(objectClass=groupPolicyContainer)", + attrs=['nTSecurityDescriptor', 'versionNumber', 'flags', 'name', 'displayName', 'gPCFileSysPath']) + except Exception, e: + raise CommandError("Failed to list policies in %s" % policies_dn, e) + for m in msg: + print("GPO : %s" % m['name'][0]) + print("display name : %s" % m['displayName'][0]) + print("path : %s" % m['gPCFileSysPath'][0]) + print("dn : %s" % m.dn) + print("version : %s" % attr_default(m, 'version', '0')) + print("flags : %s" % flags_string(gpo_flags, int(attr_default(m, 'flags', 0)))) + print("") + + +class cmd_list(Command): + """list GPOs for a user""" + + synopsis = "%prog gpo list <username>" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_args = [ 'username' ] + + takes_options = [ + Option("-H", help="LDB URL for database or target server", type=str) + ] + + def run(self, username, H=None, sambaopts=None, + credopts=None, versionopts=None, server=None): + + self.url = H + self.lp = sambaopts.get_loadparm() + + self.creds = credopts.get_credentials(self.lp, fallback_machine=True) + + samdb_connect(self) + + try: + user_dn = self.samdb.search(expression='(&(samAccountName=%s)(objectclass=User))' % username)[0].dn + except Exception, e: + raise CommandError("Failed to find user %s" % username, e) + + # check if its a computer account + try: + msg = self.samdb.search(base=user_dn, scope=ldb.SCOPE_BASE, attrs=['objectClass'])[0] + is_computer = 'computer' in msg['objectClass'] + except Exception, e: + raise CommandError("Failed to find objectClass for user %s" % username, e) + + session_info_flags = ( AUTH_SESSION_INFO_DEFAULT_GROUPS | + AUTH_SESSION_INFO_AUTHENTICATED ) + + # When connecting to a remote server, don't look up the local privilege DB + if self.url is not None and self.url.startswith('ldap'): + session_info_flags |= AUTH_SESSION_INFO_SIMPLE_PRIVILEGES + + session = samba.auth.user_session(self.samdb, lp_ctx=self.lp, dn=user_dn, + session_info_flags=session_info_flags) + + token = session.security_token + + gpos = [] + + inherit = True + dn = ldb.Dn(self.samdb, str(user_dn)).parent() + while True: + msg = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE, attrs=['gPLink', 'gPOptions'])[0] + if 'gPLink' in msg: + glist = parse_gplink(msg['gPLink'][0]) + for g in glist: + if not inherit and not (g['options'] & dsdb.GPLINK_OPT_ENFORCE): + continue + if g['options'] & dsdb.GPLINK_OPT_DISABLE: + continue + + try: + gmsg = self.samdb.search(base=g['dn'], scope=ldb.SCOPE_BASE, + attrs=['flags', 'ntSecurityDescriptor']) + except Exception: + print "Failed to fetch gpo object %s" % g['dn'] + continue + + secdesc_ndr = gmsg[0]['ntSecurityDescriptor'][0] + secdesc = ndr_unpack(dcerpc.security.descriptor, secdesc_ndr) + + try: + samba.security.access_check(secdesc, token, + dcerpc.security.SEC_STD_READ_CONTROL | + dcerpc.security.SEC_ADS_LIST | + dcerpc.security.SEC_ADS_READ_PROP) + except RuntimeError: + print "Failed access check on %s" % msg.dn + continue + + # check the flags on the GPO + flags = int(attr_default(gmsg[0], 'flags', 0)) + if is_computer and (flags & dsdb.GPO_FLAG_MACHINE_DISABLE): + continue + if not is_computer and (flags & dsdb.GPO_FLAG_USER_DISABLE): + continue + gpos.append(g) + + # check if this blocks inheritance + gpoptions = int(attr_default(msg, 'gPOptions', 0)) + if gpoptions & dsdb.GPO_BLOCK_INHERITANCE: + inherit = False + + if dn == self.samdb.get_default_basedn(): + break + dn = dn.parent() + + print("GPO's for user %s" % username) + for g in gpos: + print("\t%s" % g['dn']) + + +class cmd_gpo(SuperCommand): + """GPO commands""" + + subcommands = {} + subcommands["listall"] = cmd_listall() + subcommands["list"] = cmd_list() diff --git a/source4/scripting/python/samba/netcmd/group.py b/source4/scripting/python/samba/netcmd/group.py new file mode 100644 index 0000000000..620a7be866 --- /dev/null +++ b/source4/scripting/python/samba/netcmd/group.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python +# +# Adds a new user to a Samba4 server +# Copyright Jelmer Vernooij 2008 +# +# Based on the original in EJS: +# Copyright Andrew Tridgell 2005 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import samba.getopt as options +from samba.netcmd import Command, SuperCommand, CommandError, Option +import ldb + +from getpass import getpass +from samba.auth import system_session +from samba.samdb import SamDB +from samba.dsdb import ( + GTYPE_SECURITY_DOMAIN_LOCAL_GROUP, + GTYPE_SECURITY_GLOBAL_GROUP, + GTYPE_SECURITY_UNIVERSAL_GROUP, + GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP, + GTYPE_DISTRIBUTION_GLOBAL_GROUP, + GTYPE_DISTRIBUTION_UNIVERSAL_GROUP, +) + +security_group = dict({"Domain": GTYPE_SECURITY_DOMAIN_LOCAL_GROUP, "Global": GTYPE_SECURITY_GLOBAL_GROUP, "Universal": GTYPE_SECURITY_UNIVERSAL_GROUP}) +distribution_group = dict({"Domain": GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP, "Global": GTYPE_DISTRIBUTION_GLOBAL_GROUP, "Universal": GTYPE_DISTRIBUTION_UNIVERSAL_GROUP}) + + +class cmd_group_add(Command): + """Creates a new group""" + + synopsis = "%prog group add [options] <groupname>" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", help="LDB URL for database or target server", type=str), + Option("--groupou", + help="Alternative location (without domainDN counterpart) to default CN=Users in which new user object will be created", + type=str), + Option("--group-scope", type="choice", choices=["Domain", "Global", "Universal"], + help="Group scope (Domain | Global | Universal)"), + Option("--group-type", type="choice", choices=["Security", "Distribution"], + help="Group type (Security | Distribution)"), + Option("--description", help="Group's description", type=str), + Option("--mail-address", help="Group's email address", type=str), + Option("--notes", help="Groups's notes", type=str), + ] + + takes_args = ["groupname"] + + def run(self, groupname, credopts=None, sambaopts=None, + versionopts=None, H=None, groupou=None, group_scope=None, + group_type=None, description=None, mail_address=None, notes=None): + + if (group_type or "Security") == "Security": + gtype = security_group.get(group_scope, GTYPE_SECURITY_GLOBAL_GROUP) + else: + gtype = distribution_group.get(group_scope, GTYPE_DISTRIBUTION_GLOBAL_GROUP) + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + + try: + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + samdb.newgroup(groupname, groupou=groupou, grouptype = gtype, + description=description, mailaddress=mail_address, notes=notes) + except Exception, e: + raise CommandError('Failed to create group "%s"' % groupname, e) + + +class cmd_group_delete(Command): + """Delete a group""" + + synopsis = "%prog group delete <groupname>" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", help="LDB URL for database or target server", type=str), + ] + + takes_args = ["groupname"] + + def run(self, groupname, credopts=None, sambaopts=None, versionopts=None, H=None): + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + + try: + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + samdb.deletegroup(groupname) + except Exception, e: + raise CommandError('Failed to remove group "%s"' % groupname, e) + + +class cmd_group_add_members(Command): + """Add (comma-separated list of) group members""" + + synopsis = "%prog group addmembers <groupname> <listofmembers>" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", help="LDB URL for database or target server", type=str), + ] + + takes_args = ["groupname", "listofmembers"] + + def run(self, groupname, listofmembers, credopts=None, sambaopts=None, + versionopts=None, H=None): + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + + try: + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + samdb.add_remove_group_members(groupname, listofmembers, add_members_operation=True) + except Exception, e: + raise CommandError('Failed to add members "%s" to group "%s"' % (listofmembers, groupname), e) + + +class cmd_group_remove_members(Command): + """Remove (comma-separated list of) group members""" + + synopsis = "%prog group removemembers <groupname> <listofmembers>" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", help="LDB URL for database or target server", type=str), + ] + + takes_args = ["groupname", "listofmembers"] + + def run(self, groupname, listofmembers, credopts=None, sambaopts=None, + versionopts=None, H=None): + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + + try: + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + samdb.add_remove_group_members(groupname, listofmembers, add_members_operation=False) + except Exception, e: + raise CommandError('Failed to remove members "%s" from group "%s"' % (listofmembers, groupname), e) + + +class cmd_group(SuperCommand): + """Group management""" + + subcommands = {} + subcommands["add"] = cmd_group_add() + subcommands["delete"] = cmd_group_delete() + subcommands["addmembers"] = cmd_group_add_members() + subcommands["removemembers"] = cmd_group_remove_members() diff --git a/source4/scripting/python/samba/netcmd/join.py b/source4/scripting/python/samba/netcmd/join.py new file mode 100644 index 0000000000..507253ab81 --- /dev/null +++ b/source4/scripting/python/samba/netcmd/join.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python +# +# joins +# +# Copyright Jelmer Vernooij 2010 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import samba.getopt as options + +from samba.net import Net, LIBNET_JOIN_AUTOMATIC +from samba.netcmd import Command, CommandError, Option +from samba.dcerpc.misc import SEC_CHAN_WKSTA, SEC_CHAN_BDC +from samba.join import join_RODC, join_DC + +class cmd_join(Command): + """Joins domain as either member or backup domain controller [server connection needed]""" + + synopsis = "%prog join <dnsdomain> [DC | RODC | MEMBER] [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("--server", help="DC to join", type=str), + Option("--site", help="site to join", type=str), + ] + + takes_args = ["domain", "role?"] + + def run(self, domain, role=None, sambaopts=None, credopts=None, + versionopts=None, server=None, site=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + net = Net(creds, lp, server=credopts.ipaddress) + + if site is None: + site = "Default-First-Site-Name" + + netbios_name = lp.get("netbios name") + + if not role is None: + role = role.upper() + + if role is None or role == "MEMBER": + secure_channel_type = SEC_CHAN_WKSTA + elif role == "DC": + join_DC(server=server, creds=creds, lp=lp, domain=domain, + site=site, netbios_name=netbios_name) + return + elif role == "RODC": + join_RODC(server=server, creds=creds, lp=lp, domain=domain, + site=site, netbios_name=netbios_name) + return + else: + raise CommandError("Invalid role %s (possible values: MEMBER, BDC, RODC)" % role) + + (join_password, sid, domain_name) = net.join(domain, + netbios_name, + secure_channel_type, + LIBNET_JOIN_AUTOMATIC) + + self.outf.write("Joined domain %s (%s)\n" % (domain_name, sid)) diff --git a/source4/scripting/python/samba/netcmd/ldapcmp.py b/source4/scripting/python/samba/netcmd/ldapcmp.py new file mode 100755 index 0000000000..160aa31258 --- /dev/null +++ b/source4/scripting/python/samba/netcmd/ldapcmp.py @@ -0,0 +1,949 @@ +#!/usr/bin/env python +# +# Unix SMB/CIFS implementation. +# A command to compare differences of objects and attributes between +# two LDAP servers both running at the same time. It generally compares +# one of the three pratitions DOMAIN, CONFIGURATION or SCHEMA. Users +# that have to be provided sheould be able to read objects in any of the +# above partitions. + +# Copyright (C) Zahari Zahariev <zahari.zahariev@postpath.com> 2009, 2010 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import os +import re +import sys + +import samba +import samba.getopt as options +from samba import Ldb +from samba.ndr import ndr_pack, ndr_unpack +from samba.dcerpc import security +from ldb import SCOPE_SUBTREE, SCOPE_ONELEVEL, SCOPE_BASE, ERR_NO_SUCH_OBJECT, LdbError +from samba.netcmd import ( + Command, + CommandError, + Option, + SuperCommand, + ) + +global summary +summary = {} + +class LDAPBase(object): + + def __init__(self, host, creds, lp, + two=False, quiet=False, descriptor=False, sort_aces=False, verbose=False, + view="section", base="", scope="SUB"): + ldb_options = [] + samdb_url = host + if not "://" in host: + if os.path.isfile(host): + samdb_url = "tdb://%s" % host + else: + samdb_url = "ldap://%s" % host + # use 'paged_search' module when connecting remotely + if samdb_url.lower().startswith("ldap://"): + ldb_options = ["modules:paged_searches"] + self.ldb = Ldb(url=samdb_url, + credentials=creds, + lp=lp, + options=ldb_options) + self.search_base = base + self.search_scope = scope + self.two_domains = two + self.quiet = quiet + self.descriptor = descriptor + self.sort_aces = sort_aces + self.view = view + self.verbose = verbose + self.host = host + self.base_dn = self.find_basedn() + self.domain_netbios = self.find_netbios() + self.server_names = self.find_servers() + self.domain_name = re.sub("[Dd][Cc]=", "", self.base_dn).replace(",", ".") + self.domain_sid = self.find_domain_sid() + self.get_guid_map() + self.get_sid_map() + # + # Log some domain controller specific place-holers that are being used + # when compare content of two DCs. Uncomment for DEBUG purposes. + if self.two_domains and not self.quiet: + print "\n* Place-holders for %s:" % self.host + print 4*" " + "${DOMAIN_DN} => %s" % self.base_dn + print 4*" " + "${DOMAIN_NETBIOS} => %s" % self.domain_netbios + print 4*" " + "${SERVER_NAME} => %s" % self.server_names + print 4*" " + "${DOMAIN_NAME} => %s" % self.domain_name + + def find_domain_sid(self): + res = self.ldb.search(base=self.base_dn, expression="(objectClass=*)", scope=SCOPE_BASE) + return ndr_unpack(security.dom_sid,res[0]["objectSid"][0]) + + def find_servers(self): + """ + """ + res = self.ldb.search(base="OU=Domain Controllers,%s" % self.base_dn, \ + scope=SCOPE_SUBTREE, expression="(objectClass=computer)", attrs=["cn"]) + assert len(res) > 0 + srv = [] + for x in res: + srv.append(x["cn"][0]) + return srv + + def find_netbios(self): + res = self.ldb.search(base="CN=Partitions,CN=Configuration,%s" % self.base_dn, \ + scope=SCOPE_SUBTREE, attrs=["nETBIOSName"]) + assert len(res) > 0 + for x in res: + if "nETBIOSName" in x.keys(): + return x["nETBIOSName"][0] + + def find_basedn(self): + res = self.ldb.search(base="", expression="(objectClass=*)", scope=SCOPE_BASE, + attrs=["defaultNamingContext"]) + assert len(res) == 1 + return res[0]["defaultNamingContext"][0] + + def object_exists(self, object_dn): + res = None + try: + res = self.ldb.search(base=object_dn, scope=SCOPE_BASE) + except LdbError, (enum, estr): + if enum == ERR_NO_SUCH_OBJECT: + return False + raise + return len(res) == 1 + + def delete_force(self, object_dn): + try: + self.ldb.delete(object_dn) + except Ldb.LdbError, e: + assert "No such object" in str(e) + + def get_attribute_name(self, key): + """ Returns the real attribute name + It resolved ranged results e.g. member;range=0-1499 + """ + + r = re.compile("^([^;]+);range=(\d+)-(\d+|\*)$") + + m = r.match(key) + if m is None: + return key + + return m.group(1) + + def get_attribute_values(self, object_dn, key, vals): + """ Returns list with all attribute values + It resolved ranged results e.g. member;range=0-1499 + """ + + r = re.compile("^([^;]+);range=(\d+)-(\d+|\*)$") + + m = r.match(key) + if m is None: + # no range, just return the values + return vals + + attr = m.group(1) + hi = int(m.group(3)) + + # get additional values in a loop + # until we get a response with '*' at the end + while True: + + n = "%s;range=%d-*" % (attr, hi + 1) + res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=[n]) + assert len(res) == 1 + res = dict(res[0]) + del res["dn"] + + fm = None + fvals = None + + for key in res.keys(): + m = r.match(key) + + if m is None: + continue + + if m.group(1) != attr: + continue + + fm = m + fvals = list(res[key]) + break + + if fm is None: + break + + vals.extend(fvals) + if fm.group(3) == "*": + # if we got "*" we're done + break + + assert int(fm.group(2)) == hi + 1 + hi = int(fm.group(3)) + + return vals + + def get_attributes(self, object_dn): + """ Returns dict with all default visible attributes + """ + res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["*"]) + assert len(res) == 1 + res = dict(res[0]) + # 'Dn' element is not iterable and we have it as 'distinguishedName' + del res["dn"] + for key in res.keys(): + vals = list(res[key]) + del res[key] + name = self.get_attribute_name(key) + res[name] = self.get_attribute_values(object_dn, key, vals) + + return res + + def get_descriptor_sddl(self, object_dn): + res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["nTSecurityDescriptor"]) + desc = res[0]["nTSecurityDescriptor"][0] + desc = ndr_unpack(security.descriptor, desc) + return desc.as_sddl(self.domain_sid) + + def guid_as_string(self, guid_blob): + """ Translate binary representation of schemaIDGUID to standard string representation. + @gid_blob: binary schemaIDGUID + """ + blob = "%s" % guid_blob + stops = [4, 2, 2, 2, 6] + index = 0 + res = "" + x = 0 + while x < len(stops): + tmp = "" + y = 0 + while y < stops[x]: + c = hex(ord(blob[index])).replace("0x", "") + c = [None, "0" + c, c][len(c)] + if 2 * index < len(blob): + tmp = c + tmp + else: + tmp += c + index += 1 + y += 1 + res += tmp + " " + x += 1 + assert index == len(blob) + return res.strip().replace(" ", "-") + + def get_guid_map(self): + """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights. + """ + self.guid_map = {} + res = self.ldb.search(base="cn=schema,cn=configuration,%s" % self.base_dn, \ + expression="(schemaIdGuid=*)", scope=SCOPE_SUBTREE, attrs=["schemaIdGuid", "name"]) + for item in res: + self.guid_map[self.guid_as_string(item["schemaIdGuid"]).lower()] = item["name"][0] + # + res = self.ldb.search(base="cn=extended-rights,cn=configuration,%s" % self.base_dn, \ + expression="(rightsGuid=*)", scope=SCOPE_SUBTREE, attrs=["rightsGuid", "name"]) + for item in res: + self.guid_map[str(item["rightsGuid"]).lower()] = item["name"][0] + + def get_sid_map(self): + """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights. + """ + self.sid_map = {} + res = self.ldb.search(base="%s" % self.base_dn, \ + expression="(objectSid=*)", scope=SCOPE_SUBTREE, attrs=["objectSid", "sAMAccountName"]) + for item in res: + try: + self.sid_map["%s" % ndr_unpack(security.dom_sid, item["objectSid"][0])] = item["sAMAccountName"][0] + except KeyError: + pass + +class Descriptor(object): + def __init__(self, connection, dn): + self.con = connection + self.dn = dn + self.sddl = self.con.get_descriptor_sddl(self.dn) + self.dacl_list = self.extract_dacl() + if self.con.sort_aces: + self.dacl_list.sort() + + def extract_dacl(self): + """ Extracts the DACL as a list of ACE string (with the brakets). + """ + try: + if "S:" in self.sddl: + res = re.search("D:(.*?)(\(.*?\))S:", self.sddl).group(2) + else: + res = re.search("D:(.*?)(\(.*\))", self.sddl).group(2) + except AttributeError: + return [] + return re.findall("(\(.*?\))", res) + + def fix_guid(self, ace): + res = "%s" % ace + guids = re.findall("[a-z0-9]+?-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+", res) + # If there are not GUIDs to replace return the same ACE + if len(guids) == 0: + return res + for guid in guids: + try: + name = self.con.guid_map[guid.lower()] + res = res.replace(guid, name) + except KeyError: + # Do not bother if the GUID is not found in + # cn=Schema or cn=Extended-Rights + pass + return res + + def fix_sid(self, ace): + res = "%s" % ace + sids = re.findall("S-[-0-9]+", res) + # If there are not SIDs to replace return the same ACE + if len(sids) == 0: + return res + for sid in sids: + try: + name = self.con.sid_map[sid] + res = res.replace(sid, name) + except KeyError: + # Do not bother if the SID is not found in baseDN + pass + return res + + def fixit(self, ace): + """ Combine all replacement methods in one + """ + res = "%s" % ace + res = self.fix_guid(res) + res = self.fix_sid(res) + return res + + def diff_1(self, other): + res = "" + if len(self.dacl_list) != len(other.dacl_list): + res += 4*" " + "Difference in ACE count:\n" + res += 8*" " + "=> %s\n" % len(self.dacl_list) + res += 8*" " + "=> %s\n" % len(other.dacl_list) + # + i = 0 + flag = True + while True: + self_ace = None + other_ace = None + try: + self_ace = "%s" % self.dacl_list[i] + except IndexError: + self_ace = "" + # + try: + other_ace = "%s" % other.dacl_list[i] + except IndexError: + other_ace = "" + if len(self_ace) + len(other_ace) == 0: + break + self_ace_fixed = "%s" % self.fixit(self_ace) + other_ace_fixed = "%s" % other.fixit(other_ace) + if self_ace_fixed != other_ace_fixed: + res += "%60s * %s\n" % ( self_ace_fixed, other_ace_fixed ) + flag = False + else: + res += "%60s | %s\n" % ( self_ace_fixed, other_ace_fixed ) + i += 1 + return (flag, res) + + def diff_2(self, other): + res = "" + if len(self.dacl_list) != len(other.dacl_list): + res += 4*" " + "Difference in ACE count:\n" + res += 8*" " + "=> %s\n" % len(self.dacl_list) + res += 8*" " + "=> %s\n" % len(other.dacl_list) + # + common_aces = [] + self_aces = [] + other_aces = [] + self_dacl_list_fixed = [] + other_dacl_list_fixed = [] + [self_dacl_list_fixed.append( self.fixit(ace) ) for ace in self.dacl_list] + [other_dacl_list_fixed.append( other.fixit(ace) ) for ace in other.dacl_list] + for ace in self_dacl_list_fixed: + try: + other_dacl_list_fixed.index(ace) + except ValueError: + self_aces.append(ace) + else: + common_aces.append(ace) + self_aces = sorted(self_aces) + if len(self_aces) > 0: + res += 4*" " + "ACEs found only in %s:\n" % self.con.host + for ace in self_aces: + res += 8*" " + ace + "\n" + # + for ace in other_dacl_list_fixed: + try: + self_dacl_list_fixed.index(ace) + except ValueError: + other_aces.append(ace) + else: + common_aces.append(ace) + other_aces = sorted(other_aces) + if len(other_aces) > 0: + res += 4*" " + "ACEs found only in %s:\n" % other.con.host + for ace in other_aces: + res += 8*" " + ace + "\n" + # + common_aces = sorted(list(set(common_aces))) + if self.con.verbose: + res += 4*" " + "ACEs found in both:\n" + for ace in common_aces: + res += 8*" " + ace + "\n" + return (self_aces == [] and other_aces == [], res) + +class LDAPObject(object): + def __init__(self, connection, dn, summary): + self.con = connection + self.two_domains = self.con.two_domains + self.quiet = self.con.quiet + self.verbose = self.con.verbose + self.summary = summary + self.dn = dn.replace("${DOMAIN_DN}", self.con.base_dn) + self.dn = self.dn.replace("CN=${DOMAIN_NETBIOS}", "CN=%s" % self.con.domain_netbios) + for x in self.con.server_names: + self.dn = self.dn.replace("CN=${SERVER_NAME}", "CN=%s" % x) + self.attributes = self.con.get_attributes(self.dn) + # Attributes that are considered always to be different e.g based on timestamp etc. + # + # One domain - two domain controllers + self.ignore_attributes = [ + # Default Naming Context + "lastLogon", "lastLogoff", "badPwdCount", "logonCount", "badPasswordTime", "modifiedCount", + "operatingSystemVersion","oEMInformation", + # Configuration Naming Context + "repsFrom", "dSCorePropagationData", "msExchServer1HighestUSN", + "replUpToDateVector", "repsTo", "whenChanged", "uSNChanged", "uSNCreated", + # Schema Naming Context + "prefixMap",] + self.dn_attributes = [] + self.domain_attributes = [] + self.servername_attributes = [] + self.netbios_attributes = [] + self.other_attributes = [] + # Two domains - two domain controllers + + if self.two_domains: + self.ignore_attributes += [ + "objectCategory", "objectGUID", "objectSid", "whenCreated", "pwdLastSet", "uSNCreated", "creationTime", + "modifiedCount", "priorSetTime", "rIDManagerReference", "gPLink", "ipsecNFAReference", + "fRSPrimaryMember", "fSMORoleOwner", "masteredBy", "ipsecOwnersReference", "wellKnownObjects", + "badPwdCount", "ipsecISAKMPReference", "ipsecFilterReference", "msDs-masteredBy", "lastSetTime", + "ipsecNegotiationPolicyReference", "subRefs", "gPCFileSysPath", "accountExpires", "invocationId", + # After Exchange preps + "targetAddress", "msExchMailboxGuid", "siteFolderGUID"] + # + # Attributes that contain the unique DN tail part e.g. 'DC=samba,DC=org' + self.dn_attributes = [ + "distinguishedName", "defaultObjectCategory", "member", "memberOf", "siteList", "nCName", + "homeMDB", "homeMTA", "interSiteTopologyGenerator", "serverReference", + "msDS-HasInstantiatedNCs", "hasMasterNCs", "msDS-hasMasterNCs", "msDS-HasDomainNCs", "dMDLocation", + "msDS-IsDomainFor", "rIDSetReferences", "serverReferenceBL", + # After Exchange preps + "msExchHomeRoutingGroup", "msExchResponsibleMTAServer", "siteFolderServer", "msExchRoutingMasterDN", + "msExchRoutingGroupMembersBL", "homeMDBBL", "msExchHomePublicMDB", "msExchOwningServer", "templateRoots", + "addressBookRoots", "msExchPolicyRoots", "globalAddressList", "msExchOwningPFTree", + "msExchResponsibleMTAServerBL", "msExchOwningPFTreeBL",] + self.dn_attributes = [x.upper() for x in self.dn_attributes] + # + # Attributes that contain the Domain name e.g. 'samba.org' + self.domain_attributes = [ + "proxyAddresses", "mail", "userPrincipalName", "msExchSmtpFullyQualifiedDomainName", + "dnsHostName", "networkAddress", "dnsRoot", "servicePrincipalName",] + self.domain_attributes = [x.upper() for x in self.domain_attributes] + # + # May contain DOMAIN_NETBIOS and SERVER_NAME + self.servername_attributes = [ "distinguishedName", "name", "CN", "sAMAccountName", "dNSHostName", + "servicePrincipalName", "rIDSetReferences", "serverReference", "serverReferenceBL", + "msDS-IsDomainFor", "interSiteTopologyGenerator",] + self.servername_attributes = [x.upper() for x in self.servername_attributes] + # + self.netbios_attributes = [ "servicePrincipalName", "CN", "distinguishedName", "nETBIOSName", "name",] + self.netbios_attributes = [x.upper() for x in self.netbios_attributes] + # + self.other_attributes = [ "name", "DC",] + self.other_attributes = [x.upper() for x in self.other_attributes] + # + self.ignore_attributes = [x.upper() for x in self.ignore_attributes] + + def log(self, msg): + """ + Log on the screen if there is no --quiet oprion set + """ + if not self.quiet: + print msg + + def fix_dn(self, s): + res = "%s" % s + if not self.two_domains: + return res + if res.upper().endswith(self.con.base_dn.upper()): + res = res[:len(res)-len(self.con.base_dn)] + "${DOMAIN_DN}" + return res + + def fix_domain_name(self, s): + res = "%s" % s + if not self.two_domains: + return res + res = res.replace(self.con.domain_name.lower(), self.con.domain_name.upper()) + res = res.replace(self.con.domain_name.upper(), "${DOMAIN_NAME}") + return res + + def fix_domain_netbios(self, s): + res = "%s" % s + if not self.two_domains: + return res + res = res.replace(self.con.domain_netbios.lower(), self.con.domain_netbios.upper()) + res = res.replace(self.con.domain_netbios.upper(), "${DOMAIN_NETBIOS}") + return res + + def fix_server_name(self, s): + res = "%s" % s + if not self.two_domains or len(self.con.server_names) > 1: + return res + for x in self.con.server_names: + res = res.upper().replace(x, "${SERVER_NAME}") + return res + + def __eq__(self, other): + if self.con.descriptor: + return self.cmp_desc(other) + return self.cmp_attrs(other) + + def cmp_desc(self, other): + d1 = Descriptor(self.con, self.dn) + d2 = Descriptor(other.con, other.dn) + if self.con.view == "section": + res = d1.diff_2(d2) + elif self.con.view == "collision": + res = d1.diff_1(d2) + else: + raise Exception("Unknown --view option value.") + # + self.screen_output = res[1][:-1] + other.screen_output = res[1][:-1] + # + return res[0] + + def cmp_attrs(self, other): + res = "" + self.unique_attrs = [] + self.df_value_attrs = [] + other.unique_attrs = [] + if self.attributes.keys() != other.attributes.keys(): + # + title = 4*" " + "Attributes found only in %s:" % self.con.host + for x in self.attributes.keys(): + if not x in other.attributes.keys() and \ + not x.upper() in [q.upper() for q in other.ignore_attributes]: + if title: + res += title + "\n" + title = None + res += 8*" " + x + "\n" + self.unique_attrs.append(x) + # + title = 4*" " + "Attributes found only in %s:" % other.con.host + for x in other.attributes.keys(): + if not x in self.attributes.keys() and \ + not x.upper() in [q.upper() for q in self.ignore_attributes]: + if title: + res += title + "\n" + title = None + res += 8*" " + x + "\n" + other.unique_attrs.append(x) + # + missing_attrs = [x.upper() for x in self.unique_attrs] + missing_attrs += [x.upper() for x in other.unique_attrs] + title = 4*" " + "Difference in attribute values:" + for x in self.attributes.keys(): + if x.upper() in self.ignore_attributes or x.upper() in missing_attrs: + continue + if isinstance(self.attributes[x], list) and isinstance(other.attributes[x], list): + self.attributes[x] = sorted(self.attributes[x]) + other.attributes[x] = sorted(other.attributes[x]) + if self.attributes[x] != other.attributes[x]: + p = None + q = None + m = None + n = None + # First check if the difference can be fixed but shunting the first part + # of the DomainHostName e.g. 'mysamba4.test.local' => 'mysamba4' + if x.upper() in self.other_attributes: + p = [self.con.domain_name.split(".")[0] == j for j in self.attributes[x]] + q = [other.con.domain_name.split(".")[0] == j for j in other.attributes[x]] + if p == q: + continue + # Attribute values that are list that contain DN based values that may differ + elif x.upper() in self.dn_attributes: + m = p + n = q + if not p and not q: + m = self.attributes[x] + n = other.attributes[x] + p = [self.fix_dn(j) for j in m] + q = [other.fix_dn(j) for j in n] + if p == q: + continue + # Attributes that contain the Domain name in them + if x.upper() in self.domain_attributes: + m = p + n = q + if not p and not q: + m = self.attributes[x] + n = other.attributes[x] + p = [self.fix_domain_name(j) for j in m] + q = [other.fix_domain_name(j) for j in n] + if p == q: + continue + # + if x.upper() in self.servername_attributes: + # Attributes with SERVER_NAME + m = p + n = q + if not p and not q: + m = self.attributes[x] + n = other.attributes[x] + p = [self.fix_server_name(j) for j in m] + q = [other.fix_server_name(j) for j in n] + if p == q: + continue + # + if x.upper() in self.netbios_attributes: + # Attributes with NETBIOS Domain name + m = p + n = q + if not p and not q: + m = self.attributes[x] + n = other.attributes[x] + p = [self.fix_domain_netbios(j) for j in m] + q = [other.fix_domain_netbios(j) for j in n] + if p == q: + continue + # + if title: + res += title + "\n" + title = None + if p and q: + res += 8*" " + x + " => \n%s\n%s" % (p, q) + "\n" + else: + res += 8*" " + x + " => \n%s\n%s" % (self.attributes[x], other.attributes[x]) + "\n" + self.df_value_attrs.append(x) + # + if self.unique_attrs + other.unique_attrs != []: + assert self.unique_attrs != other.unique_attrs + self.summary["unique_attrs"] += self.unique_attrs + self.summary["df_value_attrs"] += self.df_value_attrs + other.summary["unique_attrs"] += other.unique_attrs + other.summary["df_value_attrs"] += self.df_value_attrs # they are the same + # + self.screen_output = res[:-1] + other.screen_output = res[:-1] + # + return res == "" + + +class LDAPBundel(object): + def __init__(self, connection, context, dn_list=None): + self.con = connection + self.two_domains = self.con.two_domains + self.quiet = self.con.quiet + self.verbose = self.con.verbose + self.search_base = self.con.search_base + self.search_scope = self.con.search_scope + self.summary = {} + self.summary["unique_attrs"] = [] + self.summary["df_value_attrs"] = [] + self.summary["known_ignored_dn"] = [] + self.summary["abnormal_ignored_dn"] = [] + if dn_list: + self.dn_list = dn_list + elif context.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA"]: + self.context = context.upper() + self.dn_list = self.get_dn_list(context) + else: + raise Exception("Unknown initialization data for LDAPBundel().") + counter = 0 + while counter < len(self.dn_list) and self.two_domains: + # Use alias reference + tmp = self.dn_list[counter] + tmp = tmp[:len(tmp)-len(self.con.base_dn)] + "${DOMAIN_DN}" + tmp = tmp.replace("CN=%s" % self.con.domain_netbios, "CN=${DOMAIN_NETBIOS}") + if len(self.con.server_names) == 1: + for x in self.con.server_names: + tmp = tmp.replace("CN=%s" % x, "CN=${SERVER_NAME}") + self.dn_list[counter] = tmp + counter += 1 + self.dn_list = list(set(self.dn_list)) + self.dn_list = sorted(self.dn_list) + self.size = len(self.dn_list) + + def log(self, msg): + """ + Log on the screen if there is no --quiet oprion set + """ + if not self.quiet: + print msg + + def update_size(self): + self.size = len(self.dn_list) + self.dn_list = sorted(self.dn_list) + + def __eq__(self, other): + res = True + if self.size != other.size: + self.log( "\n* DN lists have different size: %s != %s" % (self.size, other.size) ) + res = False + # + # This is the case where we want to explicitly compare two objects with different DNs. + # It does not matter if they are in the same DC, in two DC in one domain or in two + # different domains. + if self.search_scope != SCOPE_BASE: + title= "\n* DNs found only in %s:" % self.con.host + for x in self.dn_list: + if not x.upper() in [q.upper() for q in other.dn_list]: + if title: + self.log( title ) + title = None + res = False + self.log( 4*" " + x ) + self.dn_list[self.dn_list.index(x)] = "" + self.dn_list = [x for x in self.dn_list if x] + # + title= "\n* DNs found only in %s:" % other.con.host + for x in other.dn_list: + if not x.upper() in [q.upper() for q in self.dn_list]: + if title: + self.log( title ) + title = None + res = False + self.log( 4*" " + x ) + other.dn_list[other.dn_list.index(x)] = "" + other.dn_list = [x for x in other.dn_list if x] + # + self.update_size() + other.update_size() + assert self.size == other.size + assert sorted([x.upper() for x in self.dn_list]) == sorted([x.upper() for x in other.dn_list]) + self.log( "\n* Objects to be compared: %s" % self.size ) + + index = 0 + while index < self.size: + skip = False + try: + object1 = LDAPObject(connection=self.con, + dn=self.dn_list[index], + summary=self.summary) + except LdbError, (enum, estr): + if enum == ERR_NO_SUCH_OBJECT: + self.log( "\n!!! Object not found: %s" % self.dn_list[index] ) + skip = True + raise + try: + object2 = LDAPObject(connection=other.con, + dn=other.dn_list[index], + summary=other.summary) + except LdbError, (enum, estr): + if enum == ERR_NO_SUCH_OBJECT: + self.log( "\n!!! Object not found: %s" % other.dn_list[index] ) + skip = True + raise + if skip: + index += 1 + continue + if object1 == object2: + if self.con.verbose: + self.log( "\nComparing:" ) + self.log( "'%s' [%s]" % (object1.dn, object1.con.host) ) + self.log( "'%s' [%s]" % (object2.dn, object2.con.host) ) + self.log( 4*" " + "OK" ) + else: + self.log( "\nComparing:" ) + self.log( "'%s' [%s]" % (object1.dn, object1.con.host) ) + self.log( "'%s' [%s]" % (object2.dn, object2.con.host) ) + self.log( object1.screen_output ) + self.log( 4*" " + "FAILED" ) + res = False + self.summary = object1.summary + other.summary = object2.summary + index += 1 + # + return res + + def get_dn_list(self, context): + """ Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema. + Parse all DNs and filter those that are 'strange' or abnormal. + """ + if context.upper() == "DOMAIN": + search_base = "%s" % self.con.base_dn + elif context.upper() == "CONFIGURATION": + search_base = "CN=Configuration,%s" % self.con.base_dn + elif context.upper() == "SCHEMA": + search_base = "CN=Schema,CN=Configuration,%s" % self.con.base_dn + + dn_list = [] + if not self.search_base: + self.search_base = search_base + self.search_scope = self.search_scope.upper() + if self.search_scope == "SUB": + self.search_scope = SCOPE_SUBTREE + elif self.search_scope == "BASE": + self.search_scope = SCOPE_BASE + elif self.search_scope == "ONE": + self.search_scope = SCOPE_ONELEVEL + else: + raise StandardError("Wrong 'scope' given. Choose from: SUB, ONE, BASE") + if not self.search_base.upper().endswith(search_base.upper()): + raise StandardError("Invalid search base specified: %s" % self.search_base) + res = self.con.ldb.search(base=self.search_base, scope=self.search_scope, attrs=["dn"]) + for x in res: + dn_list.append(x["dn"].get_linearized()) + # + global summary + # + return dn_list + + def print_summary(self): + self.summary["unique_attrs"] = list(set(self.summary["unique_attrs"])) + self.summary["df_value_attrs"] = list(set(self.summary["df_value_attrs"])) + # + if self.summary["unique_attrs"]: + self.log( "\nAttributes found only in %s:" % self.con.host ) + self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["unique_attrs"]]) ) + # + if self.summary["df_value_attrs"]: + self.log( "\nAttributes with different values:" ) + self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["df_value_attrs"]]) ) + self.summary["df_value_attrs"] = [] + +class cmd_ldapcmp(Command): + """compare two ldap databases""" + synopsis = "ldapcmp URL1 URL2 <domain|configuration|schema> [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptionsDouble, + } + + takes_args = ["URL1", "URL2", "context1?", "context2?", "context3?"] + + takes_options = [ + Option("-w", "--two", dest="two", action="store_true", default=False, + help="Hosts are in two different domains"), + Option("-q", "--quiet", dest="quiet", action="store_true", default=False, + help="Do not print anything but relay on just exit code"), + Option("-v", "--verbose", dest="verbose", action="store_true", default=False, + help="Print all DN pairs that have been compared"), + Option("--sd", dest="descriptor", action="store_true", default=False, + help="Compare nTSecurityDescriptor attibutes only"), + Option("--sort-aces", dest="sort_aces", action="store_true", default=False, + help="Sort ACEs before comparison of nTSecurityDescriptor attribute"), + Option("--view", dest="view", default="section", + help="Display mode for nTSecurityDescriptor results. Possible values: section or collision."), + Option("--base", dest="base", default="", + help="Pass search base that will build DN list for the first DC."), + Option("--base2", dest="base2", default="", + help="Pass search base that will build DN list for the second DC. Used when --two or when compare two different DNs."), + Option("--scope", dest="scope", default="SUB", + help="Pass search scope that builds DN list. Options: SUB, ONE, BASE"), + ] + + def run(self, URL1, URL2, + context1=None, context2=None, context3=None, + two=False, quiet=False, verbose=False, descriptor=False, sort_aces=False, view="section", + base="", base2="", scope="SUB", credopts=None, sambaopts=None, versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + creds2 = credopts.get_credentials2(lp, guess=False) + if creds2.is_anonymous(): + creds2 = creds + else: + creds2.set_domain("") + creds2.set_workstation("") + if not creds.authentication_requested(): + raise CommandError("You must supply at least one username/password pair") + + # make a list of contexts to compare in + contexts = [] + if context1 is None: + if base and base2: + # If search bases are specified context is defaulted to + # DOMAIN so the given search bases can be verified. + contexts = ["DOMAIN"] + else: + # if no argument given, we compare all contexts + contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA"] + else: + for c in [context1, context2, context3]: + if c is None: + continue + if not c.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA"]: + raise CommandError("Incorrect argument: %s" % c) + contexts.append(c.upper()) + + if verbose and quiet: + raise CommandError("You cannot set --verbose and --quiet together") + if (not base and base2) or (base and not base2): + raise CommandError("You need to specify both --base and --base2 at the same time") + if descriptor and view.upper() not in ["SECTION", "COLLISION"]: + raise CommandError("Invalid --view value. Choose from: section or collision") + if not scope.upper() in ["SUB", "ONE", "BASE"]: + raise CommandError("Invalid --scope value. Choose from: SUB, ONE, BASE") + + con1 = LDAPBase(URL1, creds, lp, + two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces, + verbose=verbose,view=view, base=base, scope=scope) + assert len(con1.base_dn) > 0 + + con2 = LDAPBase(URL2, creds2, lp, + two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces, + verbose=verbose, view=view, base=base2, scope=scope) + assert len(con2.base_dn) > 0 + + status = 0 + for context in contexts: + if not quiet: + print "\n* Comparing [%s] context..." % context + + b1 = LDAPBundel(con1, context=context) + b2 = LDAPBundel(con2, context=context) + + if b1 == b2: + if not quiet: + print "\n* Result for [%s]: SUCCESS" % context + else: + if not quiet: + print "\n* Result for [%s]: FAILURE" % context + if not descriptor: + assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"]) + b2.summary["df_value_attrs"] = [] + print "\nSUMMARY" + print "---------" + b1.print_summary() + b2.print_summary() + # mark exit status as FAILURE if a least one comparison failed + status = -1 + if status != 0: + raise CommandError("Compare failed: %d" % status) diff --git a/source4/scripting/python/samba/netcmd/machinepw.py b/source4/scripting/python/samba/netcmd/machinepw.py new file mode 100644 index 0000000000..d822b22794 --- /dev/null +++ b/source4/scripting/python/samba/netcmd/machinepw.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python +# +# Machine passwords +# Copyright Jelmer Vernooij 2010 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import samba.getopt as options + +from samba import Ldb +from samba.auth import system_session +from samba.netcmd import Command, CommandError + + +class cmd_machinepw(Command): + """Gets a machine password out of our SAM""" + + synopsis = "%prog machinepw <accountname>" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_args = ["secret"] + + def run(self, secret, sambaopts=None, credopts=None, versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + url = lp.get("secrets database") + secretsdb = Ldb(url=url, session_info=system_session(), + credentials=creds, lp=lp) + + result = secretsdb.search(attrs=["secret"], + expression="(&(objectclass=primaryDomain)(samaccountname=%s))" % secret) + + if len(result) != 1: + raise CommandError("search returned %d records, expected 1" % len(result)) + + self.outf.write("%s\n" % result[0]["secret"]) diff --git a/source4/scripting/python/samba/netcmd/netacl.py b/source4/scripting/python/samba/netcmd/netacl.py new file mode 100644 index 0000000000..3f68ee756c --- /dev/null +++ b/source4/scripting/python/samba/netcmd/netacl.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +# +# Manipulate ACLs +# +# Copyright (C) Matthieu Patou <mat@matws.net> 2010 +# Copyright (C) Nadezhda Ivanova <nivanova@samba.org> 2010 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +from samba.netcmd import ( + SuperCommand, + ) +from samba.netcmd.ntacl import cmd_nt_acl +from samba.netcmd.dsacl import cmd_ds_acl + +class cmd_acl(SuperCommand): + """NT ACLs manipulation""" + + subcommands = {} + subcommands["nt"] = cmd_nt_acl() + subcommands["ds"] = cmd_ds_acl() diff --git a/source4/scripting/python/samba/netcmd/newuser.py b/source4/scripting/python/samba/netcmd/newuser.py new file mode 100644 index 0000000000..3581340577 --- /dev/null +++ b/source4/scripting/python/samba/netcmd/newuser.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python +# +# Adds a new user to a Samba4 server +# Copyright Jelmer Vernooij 2008 +# +# Based on the original in EJS: +# Copyright Andrew Tridgell 2005 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import samba.getopt as options +from samba.netcmd import Command, CommandError, Option +import ldb + +from getpass import getpass +from samba.auth import system_session +from samba.samdb import SamDB + +class cmd_newuser(Command): + """Creates a new user""" + + synopsis = "newuser [options] <username> [<password>]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", help="LDB URL for database or target server", type=str), + Option("--must-change-at-next-login", + help="Force password to be changed on next login", + action="store_true"), + Option("--use-username-as-cn", + help="Force use of username as user's CN", + action="store_true"), + Option("--userou", + help="Alternative location (without domainDN counterpart) to default CN=Users in which new user object will be created", + type=str), + Option("--surname", help="User's surname", type=str), + Option("--given-name", help="User's given name", type=str), + Option("--initials", help="User's initials", type=str), + Option("--profile-path", help="User's profile path", type=str), + Option("--script-path", help="User's logon script path", type=str), + Option("--home-drive", help="User's home drive letter", type=str), + Option("--home-directory", help="User's home directory path", type=str), + Option("--job-title", help="User's job title", type=str), + Option("--department", help="User's department", type=str), + Option("--company", help="User's company", type=str), + Option("--description", help="User's description", type=str), + Option("--mail-address", help="User's email address", type=str), + Option("--internet-address", help="User's home page", type=str), + Option("--telephone-number", help="User's phone number", type=str), + Option("--physical-delivery-office", help="User's office location", type=str), + ] + + takes_args = ["username", "password?"] + + def run(self, username, password=None, credopts=None, sambaopts=None, + versionopts=None, H=None, must_change_at_next_login=None, + use_username_as_cn=None, userou=None, surname=None, given_name=None, initials=None, + profile_path=None, script_path=None, home_drive=None, home_directory=None, + job_title=None, department=None, company=None, description=None, + mail_address=None, internet_address=None, telephone_number=None, physical_delivery_office=None): + + if password is None: + password = getpass("New Password: ") + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + + try: + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + samdb.newuser(username, password, + force_password_change_at_next_login_req=must_change_at_next_login, + useusernameascn=use_username_as_cn, userou=userou, surname=surname, givenname=given_name, initials=initials, + profilepath=profile_path, homedrive=home_drive, scriptpath=script_path, homedirectory=home_directory, + jobtitle=job_title, department=department, company=company, description=description, + mailaddress=mail_address, internetaddress=internet_address, + telephonenumber=telephone_number, physicaldeliveryoffice=physical_delivery_office) + except Exception, e: + raise CommandError('Failed to create user "%s"' % username, e) + + print("User %s created successfully" % username) diff --git a/source4/scripting/python/samba/netcmd/ntacl.py b/source4/scripting/python/samba/netcmd/ntacl.py new file mode 100644 index 0000000000..49f8fbc77f --- /dev/null +++ b/source4/scripting/python/samba/netcmd/ntacl.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python +# +# Manipulate file NT ACLs +# +# Copyright Matthieu Patou 2010 <mat@matws.net> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +from samba.credentials import DONT_USE_KERBEROS +import samba.getopt as options +from samba.dcerpc import security +from samba.ntacls import setntacl, getntacl +from samba import Ldb +from samba.ndr import ndr_unpack + +from ldb import SCOPE_BASE +import os + +from samba.auth import system_session +from samba.netcmd import ( + Command, + CommandError, + SuperCommand, + Option, + ) + +class cmd_acl_set(Command): + """Set ACLs on a file""" + synopsis = "%prog set <acl> <file> [--xattr-backend=native|tdb] [--eadb-file=file] [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_options = [ + Option("--quiet", help="Be quiet", action="store_true"), + Option("--xattr-backend", type="choice", help="xattr backend type (native fs or tdb)", + choices=["native","tdb"]), + Option("--eadb-file", help="Name of the tdb file where attributes are stored", type="string"), + ] + + takes_args = ["acl","file"] + + def run(self, acl, file, quiet=False,xattr_backend=None,eadb_file=None, + credopts=None, sambaopts=None, versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + path = os.path.join(lp.get("private dir"), lp.get("secrets database") or "secrets.ldb") + creds = credopts.get_credentials(lp) + creds.set_kerberos_state(DONT_USE_KERBEROS) + try: + ldb = Ldb(path, session_info=system_session(), credentials=creds, + lp=lp) + except Exception, e: + raise CommandError("Unable to read domain SID from configuration files", e) + attrs = ["objectSid"] + print lp.get("realm") + res = ldb.search(expression="(objectClass=*)", + base="flatname=%s,cn=Primary Domains" % lp.get("workgroup"), + scope=SCOPE_BASE, attrs=attrs) + if len(res) !=0: + domainsid = ndr_unpack(security.dom_sid, res[0]["objectSid"][0]) + setntacl(lp, file, acl, str(domainsid), xattr_backend, eadb_file) + else: + raise CommandError("Unable to read domain SID from configuration files") + + +class cmd_acl_get(Command): + """Set ACLs on a file""" + synopsis = "%prog get <file> [--as-sddl] [--xattr-backend=native|tdb] [--eadb-file=file] [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_options = [ + Option("--as-sddl", help="Output ACL in the SDDL format", action="store_true"), + Option("--xattr-backend", type="choice", help="xattr backend type (native fs or tdb)", + choices=["native","tdb"]), + Option("--eadb-file", help="Name of the tdb file where attributes are stored", type="string"), + ] + + takes_args = ["file"] + + def run(self, file, as_sddl=False, xattr_backend=None, eadb_file=None, + credopts=None, sambaopts=None, versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + acl = getntacl(lp, file, xattr_backend, eadb_file) + if as_sddl: + anysid = security.dom_sid(security.SID_NT_SELF) + print acl.info.as_sddl(anysid) + else: + acl.dump() + + +class cmd_nt_acl(SuperCommand): + """NT ACLs manipulation""" + + subcommands = {} + subcommands["set"] = cmd_acl_set() + subcommands["get"] = cmd_acl_get() + diff --git a/source4/scripting/python/samba/netcmd/pwsettings.py b/source4/scripting/python/samba/netcmd/pwsettings.py new file mode 100644 index 0000000000..3f1a8e189d --- /dev/null +++ b/source4/scripting/python/samba/netcmd/pwsettings.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python +# +# Sets password settings. +# (Password complexity, history length, minimum password length, the minimum +# and maximum password age) on a Samba4 server +# +# Copyright Matthias Dieter Wallnoefer 2009 +# Copyright Andrew Kroeger 2009 +# Copyright Jelmer Vernooij 2009 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import samba.getopt as options +import ldb + +from samba.auth import system_session +from samba.samdb import SamDB +from samba.dcerpc.samr import DOMAIN_PASSWORD_COMPLEX, DOMAIN_PASSWORD_STORE_CLEARTEXT +from samba.netcmd import Command, CommandError, Option + +class cmd_pwsettings(Command): + """Sets password settings + + Password complexity, history length, minimum password length, the minimum + and maximum password age) on a Samba4 server. + """ + + synopsis = "(show | set <options>)" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", help="LDB URL for database or target server", type=str), + Option("--quiet", help="Be quiet", action="store_true"), + Option("--complexity", type="choice", choices=["on","off","default"], + help="The password complexity (on | off | default). Default is 'on'"), + Option("--store-plaintext", type="choice", choices=["on","off","default"], + help="Store plaintext passwords where account have 'store passwords with reversible encryption' set (on | off | default). Default is 'off'"), + Option("--history-length", + help="The password history length (<integer> | default). Default is 24.", type=str), + Option("--min-pwd-length", + help="The minimum password length (<integer> | default). Default is 7.", type=str), + Option("--min-pwd-age", + help="The minimum password age (<integer in days> | default). Default is 1.", type=str), + Option("--max-pwd-age", + help="The maximum password age (<integer in days> | default). Default is 43.", type=str), + ] + + takes_args = ["subcommand"] + + def run(self, subcommand, H=None, min_pwd_age=None, max_pwd_age=None, + quiet=False, complexity=None, store_plaintext=None, history_length=None, + min_pwd_length=None, credopts=None, sambaopts=None, + versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + + domain_dn = samdb.domain_dn() + res = samdb.search(domain_dn, scope=ldb.SCOPE_BASE, + attrs=["pwdProperties", "pwdHistoryLength", "minPwdLength", + "minPwdAge", "maxPwdAge"]) + assert(len(res) == 1) + try: + pwd_props = int(res[0]["pwdProperties"][0]) + pwd_hist_len = int(res[0]["pwdHistoryLength"][0]) + cur_min_pwd_len = int(res[0]["minPwdLength"][0]) + # ticks -> days + cur_min_pwd_age = int(abs(int(res[0]["minPwdAge"][0])) / (1e7 * 60 * 60 * 24)) + cur_max_pwd_age = int(abs(int(res[0]["maxPwdAge"][0])) / (1e7 * 60 * 60 * 24)) + except Exception, e: + raise CommandError("Could not retrieve password properties!", e) + + if subcommand == "show": + self.message("Password informations for domain '%s'" % domain_dn) + self.message("") + if pwd_props & DOMAIN_PASSWORD_COMPLEX != 0: + self.message("Password complexity: on") + else: + self.message("Password complexity: off") + if pwd_props & DOMAIN_PASSWORD_STORE_CLEARTEXT != 0: + self.message("Store plaintext passwords: on") + else: + self.message("Store plaintext passwords: off") + self.message("Password history length: %d" % pwd_hist_len) + self.message("Minimum password length: %d" % cur_min_pwd_len) + self.message("Minimum password age (days): %d" % cur_min_pwd_age) + self.message("Maximum password age (days): %d" % cur_max_pwd_age) + elif subcommand == "set": + msgs = [] + m = ldb.Message() + m.dn = ldb.Dn(samdb, domain_dn) + + if complexity is not None: + if complexity == "on" or complexity == "default": + pwd_props = pwd_props | DOMAIN_PASSWORD_COMPLEX + msgs.append("Password complexity activated!") + elif complexity == "off": + pwd_props = pwd_props & (~DOMAIN_PASSWORD_COMPLEX) + msgs.append("Password complexity deactivated!") + + if store_plaintext is not None: + if store_plaintext == "on" or store_plaintext == "default": + pwd_props = pwd_props | DOMAIN_PASSWORD_STORE_CLEARTEXT + msgs.append("Plaintext password storage for changed passwords activated!") + elif store_plaintext == "off": + pwd_props = pwd_props & (~DOMAIN_PASSWORD_STORE_CLEARTEXT) + msgs.append("Plaintext password storage for changed passwords deactivated!") + + if complexity is not None or store_plaintext is not None: + m["pwdProperties"] = ldb.MessageElement(str(pwd_props), + ldb.FLAG_MOD_REPLACE, "pwdProperties") + + if history_length is not None: + if history_length == "default": + pwd_hist_len = 24 + else: + pwd_hist_len = int(history_length) + + if pwd_hist_len < 0 or pwd_hist_len > 24: + raise CommandError("Password history length must be in the range of 0 to 24!") + + m["pwdHistoryLength"] = ldb.MessageElement(str(pwd_hist_len), + ldb.FLAG_MOD_REPLACE, "pwdHistoryLength") + msgs.append("Password history length changed!") + + if min_pwd_length is not None: + if min_pwd_length == "default": + min_pwd_len = 7 + else: + min_pwd_len = int(min_pwd_length) + + if min_pwd_len < 0 or min_pwd_len > 14: + raise CommandError("Minimum password length must be in the range of 0 to 14!") + + m["minPwdLength"] = ldb.MessageElement(str(min_pwd_len), + ldb.FLAG_MOD_REPLACE, "minPwdLength") + msgs.append("Minimum password length changed!") + + if min_pwd_age is not None: + if min_pwd_age == "default": + min_pwd_age = 1 + else: + min_pwd_age = int(min_pwd_age) + + if min_pwd_age < 0 or min_pwd_age > 998: + raise CommandError("Minimum password age must be in the range of 0 to 998!") + + # days -> ticks + min_pwd_age_ticks = -int(min_pwd_age * (24 * 60 * 60 * 1e7)) + + m["minPwdAge"] = ldb.MessageElement(str(min_pwd_age_ticks), + ldb.FLAG_MOD_REPLACE, "minPwdAge") + msgs.append("Minimum password age changed!") + + if max_pwd_age is not None: + if max_pwd_age == "default": + max_pwd_age = 43 + else: + max_pwd_age = int(max_pwd_age) + + if max_pwd_age < 0 or max_pwd_age > 999: + raise CommandError("Maximum password age must be in the range of 0 to 999!") + + # days -> ticks + max_pwd_age_ticks = -int(max_pwd_age * (24 * 60 * 60 * 1e7)) + + m["maxPwdAge"] = ldb.MessageElement(str(max_pwd_age_ticks), + ldb.FLAG_MOD_REPLACE, "maxPwdAge") + msgs.append("Maximum password age changed!") + + if max_pwd_age > 0 and min_pwd_age >= max_pwd_age: + raise CommandError("Maximum password age (%d) must be greater than minimum password age (%d)!" % (max_pwd_age, min_pwd_age)) + + samdb.modify(m) + msgs.append("All changes applied successfully!") + self.message("\n".join(msgs)) + else: + raise CommandError("Wrong argument '%s'!" % subcommand) diff --git a/source4/scripting/python/samba/netcmd/rodc.py b/source4/scripting/python/samba/netcmd/rodc.py new file mode 100644 index 0000000000..f4daac853d --- /dev/null +++ b/source4/scripting/python/samba/netcmd/rodc.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python +# +# rodc related commands +# +# Copyright Andrew Tridgell 2010 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +from samba.netcmd import Command, CommandError, Option, SuperCommand +import samba.getopt as options +from samba.samdb import SamDB +from samba.auth import system_session +import ldb +from samba.dcerpc import misc, drsuapi +from samba.credentials import Credentials +from samba.drs_utils import drs_Replicate + +class cmd_rodc_preload(Command): + """Preload one account for an RODC""" + + synopsis = "%prog rodc preload <SID|DN|accountname>" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("--server", help="DC to use", type=str), + ] + + takes_args = ["account"] + + def get_dn(self, samdb, account): + '''work out what DN they meant''' + + # we accept the account in SID, accountname or DN form + if account[0:2] == 'S-': + res = samdb.search(base="<SID=%s>" % account, + expression="objectclass=user", + scope=ldb.SCOPE_BASE, attrs=[]) + elif account.find('=') >= 0: + res = samdb.search(base=account, + expression="objectclass=user", + scope=ldb.SCOPE_BASE, attrs=[]) + else: + res = samdb.search(expression="(&(samAccountName=%s)(objectclass=user))" % account, + scope=ldb.SCOPE_SUBTREE, attrs=[]) + if len(res) != 1: + raise Exception("Failed to find account '%s'" % account) + return str(res[0]["dn"]) + + + def get_dsServiceName(self, samdb): + res = samdb.search(base="", scope=ldb.SCOPE_BASE, attrs=["dsServiceName"]) + return res[0]["dsServiceName"][0] + + + def run(self, account, sambaopts=None, + credopts=None, versionopts=None, server=None): + + if server is None: + raise Exception("You must supply a server") + + lp = sambaopts.get_loadparm() + + creds = credopts.get_credentials(lp, fallback_machine=True) + + # connect to the remote and local SAMs + samdb = SamDB(url="ldap://%s" % server, + session_info=system_session(), + credentials=creds, lp=lp) + + local_samdb = SamDB(url=None, session_info=system_session(), + credentials=creds, lp=lp) + + # work out the source and destination GUIDs + dc_ntds_dn = self.get_dsServiceName(samdb) + res = samdb.search(base=dc_ntds_dn, scope=ldb.SCOPE_BASE, attrs=["invocationId"]) + source_dsa_invocation_id = misc.GUID(local_samdb.schema_format_value("objectGUID", res[0]["invocationId"][0])) + + dn = self.get_dn(samdb, account) + print "Replicating DN %s" % dn + + destination_dsa_guid = misc.GUID(local_samdb.get_ntds_GUID()) + + local_samdb.transaction_start() + repl = drs_Replicate("ncacn_ip_tcp:%s[seal,print]" % server, lp, creds, local_samdb) + try: + repl.replicate(dn, source_dsa_invocation_id, destination_dsa_guid, + exop=drsuapi.DRSUAPI_EXOP_REPL_SECRET, rodc=True) + except Exception, e: + raise CommandError("Error replicating DN %s" % dn, e) + local_samdb.transaction_commit() + + + +class cmd_rodc(SuperCommand): + """RODC commands""" + + subcommands = {} + subcommands["preload"] = cmd_rodc_preload() diff --git a/source4/scripting/python/samba/netcmd/setexpiry.py b/source4/scripting/python/samba/netcmd/setexpiry.py new file mode 100644 index 0000000000..bd8ea166fa --- /dev/null +++ b/source4/scripting/python/samba/netcmd/setexpiry.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python +# +# Sets the user password expiry on a Samba4 server +# Copyright Jelmer Vernooij 2008 +# +# Based on the original in EJS: +# Copyright Andrew Tridgell 2005 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +from samba.netcmd import Command, CommandError, Option + +import samba.getopt as options + +from samba.auth import system_session +from samba.samdb import SamDB + +class cmd_setexpiry(Command): + """Sets the expiration of a user account""" + + synopsis = "setexpiry [username] [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", help="LDB URL for database or target server", type=str), + Option("--filter", help="LDAP Filter to set password on", type=str), + Option("--days", help="Days to expiry", type=int), + Option("--noexpiry", help="Password does never expire", action="store_true"), + ] + + takes_args = ["username?"] + + def run(self, username=None, sambaopts=None, credopts=None, + versionopts=None, H=None, filter=None, days=None, noexpiry=None): + if username is None and filter is None: + raise CommandError("Either the username or '--filter' must be specified!") + + if filter is None: + filter = "(&(objectClass=user)(sAMAccountName=%s))" % (username) + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + + if days is None: + days = 0 + + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + + samdb.setexpiry(filter, days*24*3600, no_expiry_req=noexpiry) diff --git a/source4/scripting/python/samba/netcmd/setpassword.py b/source4/scripting/python/samba/netcmd/setpassword.py new file mode 100644 index 0000000000..b32b651822 --- /dev/null +++ b/source4/scripting/python/samba/netcmd/setpassword.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python +# +# Sets a user password on a Samba4 server +# Copyright Jelmer Vernooij 2008 +# +# Based on the original in EJS: +# Copyright Andrew Tridgell 2005 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import samba.getopt as options +from samba.netcmd import Command, CommandError, Option +from getpass import getpass +from samba.auth import system_session +from samba.samdb import SamDB +from samba import gensec +import ldb + +class cmd_setpassword(Command): + """(Re)sets the password on a user account""" + + synopsis = "setpassword [username] [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", help="LDB URL for database or target server", type=str), + Option("--filter", help="LDAP Filter to set password on", type=str), + Option("--newpassword", help="Set password", type=str), + Option("--must-change-at-next-login", + help="Force password to be changed on next login", + action="store_true"), + ] + + takes_args = ["username?"] + + def run(self, username=None, filter=None, credopts=None, sambaopts=None, + versionopts=None, H=None, newpassword=None, + must_change_at_next_login=None): + if filter is None and username is None: + raise CommandError("Either the username or '--filter' must be specified!") + + password = newpassword + if password is None: + password = getpass("New Password: ") + + if filter is None: + filter = "(&(objectClass=user)(sAMAccountName=%s))" % (username) + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + + creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL) + + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + + try: + samdb.setpassword(filter, password, + force_change_at_next_login=must_change_at_next_login, + username=username) + except Exception, e: + raise CommandError('Failed to set password for user "%s"' % username, e) + print "Changed password OK" diff --git a/source4/scripting/python/samba/netcmd/spn.py b/source4/scripting/python/samba/netcmd/spn.py new file mode 100644 index 0000000000..4cfa21fa03 --- /dev/null +++ b/source4/scripting/python/samba/netcmd/spn.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python +# +# spn management +# +# Copyright Matthieu Patou mat@samba.org 2010 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import samba.getopt as options +import ldb +import re +from samba import provision +from samba.samdb import SamDB +from samba.auth import system_session +from samba.netcmd import ( + Command, + CommandError, + SuperCommand, + Option + ) + +def _get_user_realm_domain(user): + """ get the realm or the domain and the base user + from user like: + * username + * DOMAIN\username + * username@REALM + """ + baseuser = user + realm = "" + domain = "" + m = re.match(r"(\w+)\\(\w+$)", user) + if m: + domain = m.group(1) + baseuser = m.group(2) + return (baseuser.lower(), domain.upper(), realm) + m = re.match(r"(\w+)@(\w+)", user) + if m: + baseuser = m.group(1) + realm = m.group(2) + return (baseuser.lower(), domain, realm.upper()) + +class cmd_spn_list(Command): + """List spns of a given user.""" + synopsis = "%prog spn list <user>" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_args = ["user"] + + def run(self, user, credopts=None, sambaopts=None, versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + paths = provision.provision_paths_from_lp(lp, lp.get("realm")) + sam = SamDB(paths.samdb, session_info=system_session(), + credentials=creds, lp=lp) + # TODO once I understand how, use the domain info to naildown + # to the correct domain + (cleaneduser, realm, domain) = _get_user_realm_domain(user) + print cleaneduser + res = sam.search(expression="samaccountname=%s" % cleaneduser, + scope=ldb.SCOPE_SUBTREE, + attrs=["servicePrincipalName"]) + if len(res) >0: + spns = res[0].get("servicePrincipalName") + found = False + flag = ldb.FLAG_MOD_ADD + if spns != None: + print "User %s has the following servicePrincipalName: " % str(res[0].dn) + for e in spns: + print "\t %s" % (str(e)) + + else: + print "User %s has no servicePrincipalName" % str(res[0].dn) + else: + raise CommandError("User %s not found" % user) + +class cmd_spn_add(Command): + """Create a new spn.""" + synopsis = "%prog spn add [--force] <name> <user>" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + takes_options = [ + Option("--force", help="Force the addition of the spn"\ + " even it exists already", action="store_true"), + ] + takes_args = ["name", "user"] + + def run(self, name, user, force=False, credopts=None, sambaopts=None, versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + paths = provision.provision_paths_from_lp(lp, lp.get("realm")) + sam = SamDB(paths.samdb, session_info=system_session(), + credentials=creds, lp=lp) + res = sam.search(expression="servicePrincipalName=%s" % name, + scope=ldb.SCOPE_SUBTREE, + ) + if len(res) != 0 and not force: + raise CommandError("Service principal %s already" + " affected to another user" % name) + + (cleaneduser, realm, domain) = _get_user_realm_domain(user) + res = sam.search(expression="samaccountname=%s" % cleaneduser, + scope=ldb.SCOPE_SUBTREE, + attrs=["servicePrincipalName"]) + if len(res) >0: + res[0].dn + msg = ldb.Message() + spns = res[0].get("servicePrincipalName") + tab = [] + found = False + flag = ldb.FLAG_MOD_ADD + if spns != None: + for e in spns: + if str(e) == name: + found = True + tab.append(str(e)) + flag = ldb.FLAG_MOD_REPLACE + tab.append(name) + msg.dn = res[0].dn + msg["servicePrincipalName"] = ldb.MessageElement(tab, flag, + "servicePrincipalName") + if not found: + sam.modify(msg) + else: + raise CommandError("Service principal %s already" + " affected to %s" % (name, user)) + else: + raise CommandError("User %s not found" % user) + + +class cmd_spn_delete(Command): + """Delete a spn.""" + synopsis = "%prog spn delete <name> [user]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_args = ["name", "user?"] + + def run(self, name, user=None, credopts=None, sambaopts=None, versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + paths = provision.provision_paths_from_lp(lp, lp.get("realm")) + sam = SamDB(paths.samdb, session_info=system_session(), + credentials=creds, lp=lp) + res = sam.search(expression="servicePrincipalName=%s" % name, + scope=ldb.SCOPE_SUBTREE, + attrs=["servicePrincipalName", "samAccountName"]) + if len(res) >0: + result = None + if user is not None: + (cleaneduser, realm, domain) = _get_user_realm_domain(user) + for elem in res: + if str(elem["samAccountName"]).lower() == cleaneduser: + result = elem + if result is None: + raise CommandError("Unable to find user %s with" + " spn %s" % (user, name)) + else: + if len(res) != 1: + listUser = "" + for r in res: + listUser = "%s\n%s" % (listUser, str(r.dn)) + raise CommandError("More than one user has the spn %s "\ + "and no specific user was specified, list of users"\ + " with this spn:%s" % (name, listUser)) + else: + result=res[0] + + + msg = ldb.Message() + spns = result.get("servicePrincipalName") + tab = [] + if spns != None: + for e in spns: + if str(e) != name: + tab.append(str(e)) + flag = ldb.FLAG_MOD_REPLACE + msg.dn = result.dn + msg["servicePrincipalName"] = ldb.MessageElement(tab, flag, + "servicePrincipalName") + sam.modify(msg) + else: + raise CommandError("Service principal %s not affected" % name) + +class cmd_spn(SuperCommand): + """SPN management [server connection needed]""" + + subcommands = {} + subcommands["add"] = cmd_spn_add() + subcommands["list"] = cmd_spn_list() + subcommands["delete"] = cmd_spn_delete() + diff --git a/source4/scripting/python/samba/netcmd/time.py b/source4/scripting/python/samba/netcmd/time.py new file mode 100644 index 0000000000..e13d3df96c --- /dev/null +++ b/source4/scripting/python/samba/netcmd/time.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# +# time +# +# Copyright Jelmer Vernooij 2010 <jelmer@samba.org> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import samba.getopt as options +import common +from samba.net import Net + +from samba.netcmd import ( + Command, + ) + +class cmd_time(Command): + """Retrieve the time on a remote server [server connection needed]""" + synopsis = "%prog time <server-name>" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_args = ["server_name?"] + + def run(self, server_name=None, credopts=None, sambaopts=None, versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + net = Net(creds, lp, server=credopts.ipaddress) + if server_name is None: + server_name = common.netcmd_dnsname(lp) + print net.time(server_name) diff --git a/source4/scripting/python/samba/netcmd/user.py b/source4/scripting/python/samba/netcmd/user.py new file mode 100644 index 0000000000..bbc972bcc7 --- /dev/null +++ b/source4/scripting/python/samba/netcmd/user.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python +# +# user management +# +# Copyright Jelmer Vernooij 2010 <jelmer@samba.org> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import samba.getopt as options + +from samba.net import Net + +from samba.netcmd import ( + Command, + SuperCommand, + ) + +class cmd_user_add(Command): + """Create a new user.""" + synopsis = "%prog user add <name> [<password>]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_args = ["name", "password?"] + + def run(self, name, password=None, credopts=None, sambaopts=None, versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp ) + net = Net(creds, lp, server=credopts.ipaddress) + net.create_user(name) + if password is not None: + net.set_password(name, creds.get_domain(), password, creds) + + +class cmd_user_delete(Command): + """Delete a user.""" + synopsis = "%prog user delete <name>" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_args = ["name"] + + def run(self, name, credopts=None, sambaopts=None, versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + net = Net(creds, lp, server=credopts.ipaddress) + net.delete_user(name) + + +class cmd_user(SuperCommand): + """User management [server connection needed]""" + + subcommands = {} + subcommands["add"] = cmd_user_add() + subcommands["delete"] = cmd_user_delete() + diff --git a/source4/scripting/python/samba/netcmd/vampire.py b/source4/scripting/python/samba/netcmd/vampire.py new file mode 100644 index 0000000000..509aa8aacd --- /dev/null +++ b/source4/scripting/python/samba/netcmd/vampire.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +# +# Vampire +# +# Copyright Jelmer Vernooij 2010 <jelmer@samba.org> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import samba.getopt as options + +from samba.net import Net + +from samba.netcmd import ( + Command, + Option, + SuperCommand, + CommandError + ) + +class cmd_vampire(Command): + """Join and synchronise a remote AD domain to the local server [server connection needed]""" + synopsis = "%prog vampire [options] <domain>" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_options = [ + Option("--target-dir", help="Target directory.", type=str), + Option("--force", help="force run", action='store_true', default=False), + ] + + takes_args = ["domain"] + + def run(self, domain, target_dir=None, credopts=None, sambaopts=None, versionopts=None, force=False): + if not force: + raise CommandError("samba-tool vampire is deprecated, please use samba-tool join. Use --force to override") + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + net = Net(creds, lp, server=credopts.ipaddress) + (domain_name, domain_sid) = net.vampire(domain=domain, target_dir=target_dir) + self.outf.write("Vampired domain %s (%s)\n" % (domain_name, domain_sid)) diff --git a/source4/scripting/python/samba/ntacls.py b/source4/scripting/python/samba/ntacls.py new file mode 100644 index 0000000000..78365e98b8 --- /dev/null +++ b/source4/scripting/python/samba/ntacls.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python + +# Unix SMB/CIFS implementation. +# Copyright (C) Matthieu Patou <mat@matws.net> 2009-2010 +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +"""NT Acls.""" + + +import os +import samba.xattr_native, samba.xattr_tdb +from samba.dcerpc import security, xattr +from samba.ndr import ndr_pack, ndr_unpack + +class XattrBackendError(Exception): + """A generic xattr backend error.""" + + +def checkset_backend(lp, backend, eadbfile): + '''return the path to the eadb, or None''' + if backend is None: + return lp.get("posix:eadb") + elif backend == "native": + return None + elif backend == "tdb": + if eadbfile is not None: + return eadbfile + else: + return os.path.abspath(os.path.join(lp.get("private dir"), "eadb.tdb")) + else: + raise XattrBackendError("Invalid xattr backend choice %s"%backend) + + +def getntacl(lp, file, backend=None, eadbfile=None): + eadbname = checkset_backend(lp, backend, eadbfile) + if eadbname is not None: + try: + attribute = samba.xattr_tdb.wrap_getxattr(eadbname, file, + xattr.XATTR_NTACL_NAME) + except Exception: + # FIXME: Don't catch all exceptions, just those related to opening + # xattrdb + print "Fail to open %s" % eadbname + attribute = samba.xattr_native.wrap_getxattr(file, + xattr.XATTR_NTACL_NAME) + else: + attribute = samba.xattr_native.wrap_getxattr(file, + xattr.XATTR_NTACL_NAME) + ntacl = ndr_unpack(xattr.NTACL, attribute) + return ntacl + + +def setntacl(lp, file, sddl, domsid, backend=None, eadbfile=None): + eadbname = checkset_backend(lp, backend, eadbfile) + ntacl = xattr.NTACL() + ntacl.version = 1 + sid = security.dom_sid(domsid) + sd = security.descriptor.from_sddl(sddl, sid) + ntacl.info = sd + if eadbname is not None: + try: + samba.xattr_tdb.wrap_setxattr(eadbname, + file, xattr.XATTR_NTACL_NAME, ndr_pack(ntacl)) + except Exception: + # FIXME: Don't catch all exceptions, just those related to opening + # xattrdb + print "Fail to open %s" % eadbname + samba.xattr_native.wrap_setxattr(file, xattr.XATTR_NTACL_NAME, + ndr_pack(ntacl)) + else: + samba.xattr_native.wrap_setxattr(file, xattr.XATTR_NTACL_NAME, + ndr_pack(ntacl)) + + +def ldapmask2filemask(ldm): + """Takes the access mask of a DS ACE and transform them in a File ACE mask. + """ + RIGHT_DS_CREATE_CHILD = 0x00000001 + RIGHT_DS_DELETE_CHILD = 0x00000002 + RIGHT_DS_LIST_CONTENTS = 0x00000004 + ACTRL_DS_SELF = 0x00000008 + RIGHT_DS_READ_PROPERTY = 0x00000010 + RIGHT_DS_WRITE_PROPERTY = 0x00000020 + RIGHT_DS_DELETE_TREE = 0x00000040 + RIGHT_DS_LIST_OBJECT = 0x00000080 + RIGHT_DS_CONTROL_ACCESS = 0x00000100 + FILE_READ_DATA = 0x0001 + FILE_LIST_DIRECTORY = 0x0001 + FILE_WRITE_DATA = 0x0002 + FILE_ADD_FILE = 0x0002 + FILE_APPEND_DATA = 0x0004 + FILE_ADD_SUBDIRECTORY = 0x0004 + FILE_CREATE_PIPE_INSTANCE = 0x0004 + FILE_READ_EA = 0x0008 + FILE_WRITE_EA = 0x0010 + FILE_EXECUTE = 0x0020 + FILE_TRAVERSE = 0x0020 + FILE_DELETE_CHILD = 0x0040 + FILE_READ_ATTRIBUTES = 0x0080 + FILE_WRITE_ATTRIBUTES = 0x0100 + DELETE = 0x00010000 + READ_CONTROL = 0x00020000 + WRITE_DAC = 0x00040000 + WRITE_OWNER = 0x00080000 + SYNCHRONIZE = 0x00100000 + STANDARD_RIGHTS_ALL = 0x001F0000 + + filemask = ldm & STANDARD_RIGHTS_ALL + + if (ldm & RIGHT_DS_READ_PROPERTY) and (ldm & RIGHT_DS_LIST_CONTENTS): + filemask = filemask | (SYNCHRONIZE | FILE_LIST_DIRECTORY |\ + FILE_READ_ATTRIBUTES | FILE_READ_EA |\ + FILE_READ_DATA | FILE_EXECUTE) + + if ldm & RIGHT_DS_WRITE_PROPERTY: + filemask = filemask | (SYNCHRONIZE | FILE_WRITE_DATA |\ + FILE_APPEND_DATA | FILE_WRITE_EA |\ + FILE_WRITE_ATTRIBUTES | FILE_ADD_FILE |\ + FILE_ADD_SUBDIRECTORY) + + if ldm & RIGHT_DS_CREATE_CHILD: + filemask = filemask | (FILE_ADD_SUBDIRECTORY | FILE_ADD_FILE) + + if ldm & RIGHT_DS_DELETE_CHILD: + filemask = filemask | FILE_DELETE_CHILD + + return filemask + + +def dsacl2fsacl(dssddl, domsid): + """ + + This function takes an the SDDL representation of a DS + ACL and return the SDDL representation of this ACL adapted + for files. It's used for Policy object provision + """ + sid = security.dom_sid(domsid) + ref = security.descriptor.from_sddl(dssddl, sid) + fdescr = security.descriptor() + fdescr.owner_sid = ref.owner_sid + fdescr.group_sid = ref.group_sid + fdescr.type = ref.type + fdescr.revision = ref.revision + fdescr.sacl = ref.sacl + aces = ref.dacl.aces + for i in range(0, len(aces)): + ace = aces[i] + if not ace.type & security.SEC_ACE_TYPE_ACCESS_ALLOWED_OBJECT and str(ace.trustee) != security.SID_BUILTIN_PREW2K: + # if fdescr.type & security.SEC_DESC_DACL_AUTO_INHERITED: + ace.flags = ace.flags | security.SEC_ACE_FLAG_OBJECT_INHERIT | security.SEC_ACE_FLAG_CONTAINER_INHERIT + if str(ace.trustee) == security.SID_CREATOR_OWNER: + # For Creator/Owner the IO flag is set as this ACE has only a sense for child objects + ace.flags = ace.flags | security.SEC_ACE_FLAG_INHERIT_ONLY + ace.access_mask = ldapmask2filemask(ace.access_mask) + fdescr.dacl_add(ace) + + return fdescr.as_sddl(sid) diff --git a/source4/scripting/python/samba/provision.py b/source4/scripting/python/samba/provision.py deleted file mode 100644 index 64491c2b18..0000000000 --- a/source4/scripting/python/samba/provision.py +++ /dev/null @@ -1,2067 +0,0 @@ -# -# Unix SMB/CIFS implementation. -# backend code for provisioning a Samba4 server - -# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2008 -# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2008-2009 -# Copyright (C) Oliver Liebel <oliver@itc.li> 2008-2009 -# -# Based on the original in EJS: -# Copyright (C) Andrew Tridgell <tridge@samba.org> 2005 -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. -# - -"""Functions for setting up a Samba configuration.""" - -from base64 import b64encode -import os -import sys -import pwd -import grp -import time -import uuid, glue -import socket -import param -import registry -import samba -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 -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__) - if "/site-packages/" in dirname: - prefix = "/".join(dirname[:dirname.index("/site-packages/")].split("/")[:-2]) - for suffix in ["share/setup", "share/samba/setup", "setup"]: - ret = os.path.join(prefix, suffix) - if os.path.isdir(ret): - return ret - # In source tree - ret = os.path.join(dirname, "../../../setup") - if os.path.isdir(ret): - 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): - super(InvalidNetbiosName, self).__init__("The name '%r' is not a valid NetBIOS name" % name) - - -class ProvisionPaths(object): - def __init__(self): - self.shareconf = None - self.hklm = None - self.hkcu = None - self.hkcr = None - self.hku = None - self.hkpd = None - self.hkpt = None - self.samdb = None - self.idmapdb = None - self.secrets = None - self.keytab = None - self.dns_keytab = None - self.dns = None - self.winsdb = None - self.private_dir = None - self.ldapdir = None - self.slapdconf = None - self.modulesconf = None - 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.olslapd = None - self.olcseedldif = None - - -class ProvisionNames(object): - def __init__(self): - self.rootdn = None - self.domaindn = None - self.configdn = None - self.schemadn = None - self.sambadn = None - self.ldapmanagerdn = None - self.dnsdomain = None - self.realm = None - self.netbiosname = None - self.domain = None - self.hostname = None - self.sitename = None - self.smbconf = None - - -class ProvisionResult(object): - def __init__(self): - self.paths = None - 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. - - :param lp: Loadparm context - :param session_info: Session information - :param credentials: Credentials - """ - if lp.get("realm") == "": - raise Exception("Realm empty") - ldb = Ldb(lp.get("sam database"), session_info=session_info, - credentials=credentials, lp=lp) - if len(ldb.search("(cn=Administrator)")) != 1: - raise ProvisioningError("No administrator account found") - - -def findnss(nssfn, names): - """Find a user or group from a list of possibilities. - - :param nssfn: NSS Function to try (should raise KeyError if not found) - :param names: Names to check. - :return: Value return by first names list. - """ - for name in names: - try: - return nssfn(name) - except KeyError: - pass - raise KeyError("Unable to find user/group %r" % names) - - -findnss_uid = lambda names: findnss(pwd.getpwnam, names)[2] -findnss_gid = lambda names: findnss(grp.getgrnam, names)[2] - - -def read_and_sub_file(file, subst_vars): - """Read a file and sub in variables found in it - - :param file: File to be read (typically from setup directory) - param subst_vars: Optional variables to subsitute in the file. - """ - data = open(file, 'r').read() - if subst_vars is not None: - data = substitute_var(data, subst_vars) - check_all_substituted(data) - return data - - -def setup_add_ldif(ldb, ldif_path, subst_vars=None): - """Setup a ldb in the private dir. - - :param ldb: LDB file to import data into - :param ldif_path: Path of the LDIF file to load - :param subst_vars: Optional variables to subsitute in LDIF. - """ - assert isinstance(ldif_path, str) - - data = read_and_sub_file(ldif_path, subst_vars) - ldb.add_ldif(data) - - -def setup_modify_ldif(ldb, ldif_path, subst_vars=None): - """Modify a ldb in the private dir. - - :param ldb: LDB object. - :param ldif_path: LDIF file path. - :param subst_vars: Optional dictionary with substitution variables. - """ - data = read_and_sub_file(ldif_path, subst_vars) - - ldb.modify_ldif(data) - - -def setup_ldb(ldb, ldif_path, subst_vars): - """Import a LDIF a file into a LDB handle, optionally substituting variables. - - :note: Either all LDIF data will be added or none (using transactions). - - :param ldb: LDB file to import into. - :param ldif_path: Path to the LDIF file. - :param subst_vars: Dictionary with substitution variables. - """ - assert ldb is not None - ldb.transaction_start() - try: - setup_add_ldif(ldb, ldif_path, subst_vars) - except: - ldb.transaction_cancel() - raise - ldb.transaction_commit() - - -def setup_file(template, fname, subst_vars): - """Setup a file in the private dir. - - :param template: Path of the template file. - :param fname: Path of the file to create. - :param subst_vars: Substitution variables. - """ - f = fname - - if os.path.exists(f): - os.unlink(f) - - data = read_and_sub_file(template, subst_vars) - open(f, 'w').write(data) - - -def provision_paths_from_lp(lp, dnsdomain): - """Set the default paths for provisioning. - - :param lp: Loadparm context. - :param dnsdomain: DNS Domain name - """ - paths = ProvisionPaths() - paths.private_dir = lp.get("private dir") - paths.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.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") - paths.krb5conf = os.path.join(paths.private_dir, "krb5.conf") - paths.winsdb = os.path.join(paths.private_dir, "wins.ldb") - paths.s4_ldapi_path = os.path.join(paths.private_dir, "ldapi") - paths.phpldapadminconfig = os.path.join(paths.private_dir, - "phpldapadmin-config.php") - paths.ldapdir = os.path.join(paths.private_dir, - "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, - "memberof.conf") - paths.fedoradsinf = os.path.join(paths.ldapdir, - "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, - "mmr_syncrepl.conf") - paths.olcdir = os.path.join(paths.ldapdir, - "slapd.d") - paths.olcseedldif = os.path.join(paths.ldapdir, - "olc_seed.ldif") - paths.hklm = "hklm.ldb" - paths.hkcr = "hkcr.ldb" - paths.hkcu = "hkcu.ldb" - paths.hku = "hku.ldb" - paths.hkpd = "hkpd.ldb" - paths.hkpt = "hkpt.ldb" - - paths.sysvol = lp.get("path", "sysvol") - - paths.netlogon = lp.get("path", "netlogon") - - paths.smbconf = lp.configfile - - return paths - - -def 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: - hostname = socket.gethostname().split(".")[0].lower() - - netbiosname = hostname.upper() - if not valid_netbios_name(netbiosname): - raise InvalidNetbiosName(netbiosname) - - hostname = hostname.lower() - - if dnsdomain is None: - dnsdomain = lp.get("realm") - - if serverrole is None: - serverrole = lp.get("server role") - - assert dnsdomain is not None - realm = dnsdomain.upper() - - if lp.get("realm").upper() != realm: - raise Exception("realm '%s' in %s must match chosen realm '%s'" % - (lp.get("realm"), lp.configfile, realm)) - - dnsdomain = dnsdomain.lower() - - if serverrole == "domain controller": - if domain is None: - domain = lp.get("workgroup") - if domaindn is None: - domaindn = "DC=" + dnsdomain.replace(".", ",DC=") - if lp.get("workgroup").upper() != domain.upper(): - raise Exception("workgroup '%s' in smb.conf must match chosen domain '%s'", - lp.get("workgroup"), domain) - else: - domain = netbiosname - if domaindn is None: - domaindn = "CN=" + netbiosname - - assert domain is not None - domain = domain.upper() - 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 - - if configdn is None: - 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 - - names = ProvisionNames() - names.rootdn = rootdn - names.domaindn = domaindn - names.configdn = configdn - names.schemadn = schemadn - names.sambadn = sambadn - names.ldapmanagerdn = "CN=Manager," + rootdn - names.dnsdomain = dnsdomain - names.domain = domain - names.realm = realm - names.netbiosname = netbiosname - names.hostname = hostname - names.sitename = sitename - names.serverdn = "CN=%s,CN=Servers,CN=%s,CN=Sites,%s" % (netbiosname, sitename, configdn) - - return names - - -def make_smbconf(smbconf, setup_path, hostname, domain, realm, serverrole, - targetdir): - """Create a new smb.conf file based on a couple of basic settings. - """ - assert smbconf is not None - if hostname is None: - hostname = socket.gethostname().split(".")[0].lower() - - if serverrole is None: - serverrole = "standalone" - - assert serverrole in ("domain controller", "member server", "standalone") - if serverrole == "domain controller": - smbconfsuffix = "dc" - elif serverrole == "member server": - smbconfsuffix = "member" - elif serverrole == "standalone": - smbconfsuffix = "standalone" - - assert domain is not None - assert realm is not None - - default_lp = param.LoadParm() - #Load non-existant file - if os.path.exists(smbconf): - default_lp.load(smbconf) - - if targetdir is not None: - privatedir_line = "private dir = " + os.path.abspath(os.path.join(targetdir, "private")) - lockdir_line = "lock dir = " + os.path.abspath(targetdir) - - default_lp.set("lock dir", os.path.abspath(targetdir)) - else: - privatedir_line = "" - lockdir_line = "" - - sysvol = os.path.join(default_lp.get("lock dir"), "sysvol") - netlogon = os.path.join(sysvol, realm.lower(), "scripts") - - setup_file(setup_path("provision.smb.conf.%s" % smbconfsuffix), - smbconf, { - "HOSTNAME": hostname, - "DOMAIN": domain, - "REALM": realm, - "SERVERROLE": serverrole, - "NETLOGONPATH": netlogon, - "SYSVOLPATH": sysvol, - "PRIVATEDIR_LINE": privatedir_line, - "LOCKDIR_LINE": lockdir_line - }) - - -def setup_name_mappings(samdb, idmap, sid, domaindn, root_uid, nobody_uid, - users_gid, wheel_gid): - """setup reasonable name mappings for sam names to unix names. - - :param samdb: SamDB object. - :param idmap: IDmap db object. - :param sid: The domain sid. - :param domaindn: The domain DN. - :param root_uid: uid of the UNIX root user. - :param nobody_uid: uid of the UNIX nobody user. - :param users_gid: gid of the UNIX users group. - :param wheel_gid: gid of the UNIX wheel group.""" - - 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, - erase=False): - """Setup the partitions for the SAM database. - - Alternatively, provision() may call this, and then populate the database. - - :note: This will wipe the Sam Database! - - :note: This function always removes the local SAM LDB file. The erase - parameter controls whether to erase the existing data, which - may not be stored locally but in LDAP. - """ - assert session_info is not None - - # We use options=["modules:"] to stop the modules loading - we - # just want to wipe and re-initialise the database, not start it up - - try: - samdb = Ldb(url=samdb_path, session_info=session_info, - credentials=credentials, lp=lp, options=["modules:"]) - # Wipes the database - samdb.erase_except_schema_controlled() - except LdbError: - os.unlink(samdb_path) - samdb = Ldb(url=samdb_path, session_info=session_info, - credentials=credentials, lp=lp, options=["modules:"]) - # Wipes the database - samdb.erase_except_schema_controlled() - - - #Add modules to the list to activate them by default - #beware often order is important - # - # Some Known ordering constraints: - # - rootdse must be first, as it makes redirects from "" -> cn=rootdse - # - objectclass must be before password_hash, because password_hash checks - # that the objectclass is of type person (filled in by objectclass - # module when expanding the objectclass list) - # - partition must be last - # - each partition has its own module list then - modules_list = ["resolve_oids", - "rootdse", - "acl", - "paged_results", - "ranged_results", - "anr", - "server_sort", - "asq", - "extended_dn_store", - "extended_dn_in", - "rdn_name", - "objectclass", - "descriptor", - "samldb", - "password_hash", - "operational", - "kludge_acl"] - tdb_modules_list = [ - "subtree_rename", - "subtree_delete", - "linked_attributes", - "extended_dn_out_ldb"] - modules_list2 = ["show_deleted", - "partition"] - - domaindn_ldb = "users.ldb" - configdn_ldb = "configuration.ldb" - schemadn_ldb = "schema.ldb" - if ldap_backend is not None: - domaindn_ldb = ldap_backend.ldapi_uri - configdn_ldb = ldap_backend.ldapi_uri - schemadn_ldb = ldap_backend.ldapi_uri - - 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": - tdb_modules_list.insert(0, "repl_meta_data") - backend_modules = [] - else: - backend_modules = ["objectguid"] - - if tdb_modules_list is None: - tdb_modules_list_as_string = "" - else: - tdb_modules_list_as_string = ","+",".join(tdb_modules_list) - - 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, - "SCHEMADN_MOD2": ",objectguid", - "CONFIGDN": names.configdn, - "CONFIGDN_LDB": configdn_ldb, - "DOMAINDN": names.domaindn, - "DOMAINDN_LDB": domaindn_ldb, - "SCHEMADN_MOD": "schema_fsmo,instancetype", - "CONFIGDN_MOD": "naming_fsmo,instancetype", - "DOMAINDN_MOD": "pdc_fsmo,instancetype", - "MODULES_LIST": ",".join(modules_list), - "TDB_MODULES_LIST": tdb_modules_list_as_string, - "MODULES_LIST2": ",".join(modules_list2), - "BACKEND_MOD": ",".join(backend_modules), - }) - - samdb.load_ldif_file_add(setup_path("provision_init.ldif")) - - message("Setting up sam.ldb rootDSE") - setup_samdb_rootdse(samdb, setup_path, names) - - except: - samdb.transaction_cancel() - raise - - samdb.transaction_commit() - -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"] - - 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_dns.ldif"), { - "REALM": realm, - "DNSDOMAIN": dnsdomain, - "DNS_KEYTAB": dns_keytab_path, - "DNSPASS_B64": b64encode(dnspass), - }) - - -def setup_secretsdb(path, setup_path, session_info, credentials, lp): - """Setup the secrets database. - - :param path: Path to the secrets database. - :param setup_path: Get the path to a setup file. - :param session_info: Session info. - :param credentials: Credentials - :param lp: Loadparm context - :return: LDB handle for the created secrets database - """ - if os.path.exists(path): - os.unlink(path) - secrets_ldb = Ldb(path, session_info=session_info, credentials=credentials, - lp=lp) - secrets_ldb.erase() - 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(): - if credentials.get_bind_dn() is not None: - setup_add_ldif(secrets_ldb, setup_path("secrets_simple_ldap.ldif"), { - "LDAPMANAGERDN": credentials.get_bind_dn(), - "LDAPMANAGERPASS_B64": b64encode(credentials.get_password()) - }) - else: - setup_add_ldif(secrets_ldb, setup_path("secrets_sasl_ldap.ldif"), { - "LDAPADMINUSER": credentials.get_username(), - "LDAPADMINREALM": credentials.get_realm(), - "LDAPADMINPASS_B64": b64encode(credentials.get_password()) - }) - - return secrets_ldb - -def setup_registry(path, setup_path, session_info, lp): - """Setup the registry. - - :param path: Path to the registry database - :param setup_path: Function that returns the path to a setup. - :param session_info: Session information - :param credentials: Credentials - :param lp: Loadparm context - """ - reg = registry.Registry() - hive = registry.open_ldb(path, session_info=session_info, - 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, lp): - """Setup the idmap database. - - :param path: path to the idmap database - :param setup_path: Function that returns a path to a setup file - :param session_info: Session information - :param credentials: Credentials - :param lp: Loadparm context - """ - if os.path.exists(path): - os.unlink(path) - - idmap_ldb = IDmapDB(path, session_info=session_info, - lp=lp) - - idmap_ldb.erase() - idmap_ldb.load_ldif_file_add(setup_path("idmap_init.ldif")) - return idmap_ldb - - -def setup_samdb_rootdse(samdb, setup_path, names): - """Setup the SamDB rootdse. - - :param samdb: Sam Database handle - :param setup_path: Obtain setup path - """ - setup_add_ldif(samdb, setup_path("provision_rootdse_add.ldif"), { - "SCHEMADN": names.schemadn, - "NETBIOSNAME": names.netbiosname, - "DNSDOMAIN": names.dnsdomain, - "REALM": names.realm, - "DNSNAME": "%s.%s" % (names.hostname, names.dnsdomain), - "DOMAINDN": names.domaindn, - "ROOTDN": names.rootdn, - "CONFIGDN": names.configdn, - "SERVERDN": names.serverdn, - }) - - -def setup_self_join(samdb, names, - machinepass, dnspass, - domainsid, invocationid, setup_path, - 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"), { - "CONFIGDN": names.configdn, - "SCHEMADN": names.schemadn, - "DOMAINDN": names.domaindn, - "SERVERDN": names.serverdn, - "INVOCATIONID": invocationid, - "NETBIOSNAME": names.netbiosname, - "DEFAULTSITE": names.sitename, - "DNSNAME": "%s.%s" % (names.hostname, names.dnsdomain), - "MACHINEPASS_B64": b64encode(machinepass), - "DNSPASS_B64": b64encode(dnspass), - "REALM": names.realm, - "DOMAIN": names.domain, - "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, domainguid, policyguid, policyguid_dc, - fill, adminpass, krbtgtpass, - machinepass, invocationid, dnspass, - serverrole, schema=None, ldap_backend=None): - """Setup a complete SAM Database. - - :note: This will wipe the main SAM database file! - """ - - # 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) - - 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") - - # 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("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, - "DOMAIN_OC": domain_oc - }) - - message("Modifying DomainDN: " + names.domaindn + "") - if domainguid is not None: - domainguid_mod = "replace: objectGUID\nobjectGUID: %s\n-" % domainguid - else: - domainguid_mod = "" - - setup_modify_ldif(samdb, setup_path("provision_basedn_modify.ldif"), { - "CREATTIME": str(int(time.time()) * 1e7), # seconds -> ticks - "DOMAINSID": str(domainsid), - "SCHEMADN": names.schemadn, - "NETBIOSNAME": names.netbiosname, - "DEFAULTSITE": names.sitename, - "CONFIGDN": names.configdn, - "SERVERDN": names.serverdn, - "POLICYGUID": policyguid, - "DOMAINDN": names.domaindn, - "DOMAINGUID_MOD": domainguid_mod, - "DOMAIN_FUNCTIONALITY": str(domainFunctionality), - "SAMBA_VERSION_STRING": version - }) - - message("Adding configuration container") - descr = get_config_descriptor(domainsid); - setup_add_ldif(samdb, setup_path("provision_configuration_basedn.ldif"), { - "CONFIGDN": names.configdn, - "DESCRIPTOR": descr, - }) - message("Modifying configuration container") - setup_modify_ldif(samdb, setup_path("provision_configuration_basedn_modify.ldif"), { - "CONFIGDN": names.configdn, - "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}) - - message("Setting up sam.ldb configuration data") - setup_add_ldif(samdb, setup_path("provision_configuration.ldif"), { - "CONFIGDN": names.configdn, - "NETBIOSNAME": names.netbiosname, - "DEFAULTSITE": names.sitename, - "DNSDOMAIN": names.dnsdomain, - "DOMAIN": names.domain, - "SCHEMADN": names.schemadn, - "DOMAINDN": names.domaindn, - "SERVERDN": names.serverdn, - "FOREST_FUNCTIONALALITY": str(forestFunctionality) - }) - - message("Setting up display specifiers") - display_specifiers_ldif = read_ms_ldif(setup_path('display-specifiers/DisplaySpecifiers-Win2k8R2.txt')) - display_specifiers_ldif = substitute_var(display_specifiers_ldif, {"CONFIGDN": names.configdn}) - check_all_substituted(display_specifiers_ldif) - samdb.add_ldif(display_specifiers_ldif) - - 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") - setup_add_ldif(samdb, setup_path("provision_computers_add.ldif"), { - "DOMAINDN": names.domaindn}) - message("Modifying computers container") - setup_modify_ldif(samdb, setup_path("provision_computers_modify.ldif"), { - "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, - "POLICYGUID_DC": policyguid_dc - }) - - if fill == FILL_FULL: - message("Setting up sam.ldb users and groups") - setup_add_ldif(samdb, setup_path("provision_users.ldif"), { - "DOMAINDN": names.domaindn, - "DOMAINSID": str(domainsid), - "CONFIGDN": names.configdn, - "ADMINPASS_B64": b64encode(adminpass), - "KRBTGTPASS_B64": b64encode(krbtgtpass), - }) - - if serverrole == "domain controller": - message("Setting up self join") - setup_self_join(samdb, names=names, invocationid=invocationid, - dnspass=dnspass, - machinepass=machinepass, - domainsid=domainsid, policyguid=policyguid, - 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() - raise - - samdb.transaction_commit() - return samdb - - -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, - rootdn=None, domaindn=None, schemadn=None, configdn=None, - serverdn=None, - domain=None, hostname=None, hostip=None, hostip6=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_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) - - if domainsid is None: - 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: - krbtgtpass = glue.generate_random_str(12) - if machinepass is None: - 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"]) - if wheel is None: - wheel_gid = findnss_gid(["wheel", "adm"]) - else: - wheel_gid = findnss_gid([wheel]) - - 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() - - # 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) - - lp = param.LoadParm() - lp.load(smbconf) - - names = guess_names(lp=lp, hostname=hostname, domain=domain, - dnsdomain=realm, serverrole=serverrole, sitename=sitename, - rootdn=rootdn, domaindn=domaindn, configdn=configdn, schemadn=schemadn, - serverdn=serverdn) - - paths = provision_paths_from_lp(lp, names.dnsdomain) - - if hostip is None: - try: - hostip = socket.getaddrinfo(names.hostname, None, socket.AF_INET, socket.AI_CANONNAME, socket.IPPROTO_IP)[0][-1][0] - except socket.gaierror, (socket.EAI_NODATA, msg): - hostip = None - - if hostip6 is None: - try: - hostip6 = socket.getaddrinfo(names.hostname, None, socket.AF_INET6, socket.AI_CANONNAME, socket.IPPROTO_IP)[0][-1][0] - except socket.gaierror, (socket.EAI_NODATA, msg): - hostip6 = None - - if serverrole is None: - serverrole = lp.get("server role") - - assert serverrole in ("domain controller", "member server", "standalone") - if invocationid is None and serverrole == "domain controller": - invocationid = str(uuid.uuid4()) - - if not os.path.exists(paths.private_dir): - os.mkdir(paths.private_dir) - - ldapi_url = "ldapi://%s" % urllib.quote(paths.s4_ldapi_path, 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") - share_ldb = Ldb(paths.shareconf, session_info=session_info, - credentials=credentials, lp=lp) - share_ldb.load_ldif_file_add(setup_path("share.ldif")) - - - message("Setting up secrets.ldb") - secrets_ldb = setup_secretsdb(paths.secrets, setup_path, - session_info=session_info, - credentials=secrets_credentials, lp=lp) - - message("Setting up the registry") - setup_registry(paths.hklm, setup_path, session_info, - lp=lp) - - message("Setting up idmap db") - idmap = setup_idmapdb(paths.idmapdb, setup_path, session_info=session_info, - 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, - 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=provision_backend) - - 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" % - ( paths.smbconf, setup_path("provision.smb.conf.dc"))) - assert(paths.netlogon is not None) - - if paths.sysvol is None: - message("Existing smb.conf does not have a [sysvol] share, but you are configuring a DC.") - message("Please either remove %s or see the template at %s" % - (paths.smbconf, setup_path("provision.smb.conf.dc"))) - assert(paths.sysvol is not None) - - # 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( - "[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) - - if samdb_fill == FILL_FULL: - setup_name_mappings(samdb, idmap, str(domainsid), names.domaindn, - root_uid=root_uid, nobody_uid=nobody_uid, - users_gid=users_gid, wheel_gid=wheel_gid) - - message("Setting up sam.ldb rootDSE marking as synchronized") - setup_modify_ldif(samdb, setup_path("provision_rootdse_modify.ldif")) - - # Only make a zone file on the first DC, it should be replicated with DNS replication - if serverrole == "domain controller": - 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) - - 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, ntdsguid=names.ntdsguid) - - create_named_conf(paths.namedconf, setup_path, realm=names.realm, - dnsdomain=names.dnsdomain, private_dir=paths.private_dir) - - create_named_txt(paths.namedtxt, setup_path, realm=names.realm, - dnsdomain=names.dnsdomain, private_dir=paths.private_dir, - keytab_name=paths.dns_keytab) - 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) - 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)) - if samdb_fill == FILL_FULL: - 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 - result.lp = lp - result.samdb = samdb - 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, - domain=None, hostname=None, domainsid=None, - adminpass=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, 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) - - -def setup_db_config(setup_path, dbdir): - """Setup a Berkeley database. - - :param setup_path: Setup path function. - :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) - - 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 - - # 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!") - - 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") - - self.credentials.set_password(ldapadminpass) - self.adminCredentials.set_username("samba-admin") - self.adminCredentials.set_password(ldapadminpass) - - # 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) - - 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 - - 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" - - 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" : 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 = "" - - - 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 }) - # 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}) - - 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(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()))} ) - - - 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 = "ldap://" + names.hostname + "." + names.dnsdomain +":%d" % ldap_backend_extra_port - else: - server_port_string = "" - - # Prepare the 'result' information - the commands to return in particular - result.slapd_provision_command = [slapd_path] - - result.slapd_provision_command.append("-F" + paths.olcdir) - - result.slapd_provision_command.append("-h") - - # 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 - - result.slapd_command.append(uris) - - # 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) - - # Finally, convert the configuration into cn=config style! - if not os.path.isdir(paths.olcdir): - os.makedirs(paths.olcdir, 0770) - - 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: - 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. - - :param path: Path to write the configuration to. - :param setup_path: Function to generate setup paths. - """ - setup_file(setup_path("phpldapadmin-config.php"), path, - {"S4_LDAPI_URI": ldapi_uri}) - - -def create_zone_file(path, setup_path, dnsdomain, domaindn, - 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. - :param setup_path: Setup path function. - :param dnsdomain: DNS Domain name - :param domaindn: DN of the Domain - :param hostip: Local IPv4 IP - :param hostip6: Local IPv6 IP - :param hostname: Local hostname - :param dnspass: Password for DNS - :param realm: Realm name - :param domainguid: GUID of the domain. - :param ntdsguid: GUID of the hosts nTDSDSA record. - """ - assert isinstance(domainguid, str) - - if hostip6 is not None: - hostip6_base_line = " IN AAAA " + hostip6 - hostip6_host_line = hostname + " IN AAAA " + hostip6 - else: - hostip6_base_line = "" - hostip6_host_line = "" - - if hostip is not None: - hostip_base_line = " IN A " + hostip - hostip_host_line = hostname + " IN A " + hostip - else: - hostip_base_line = "" - hostip_host_line = "" - - setup_file(setup_path("provision.zone"), path, { - "DNSPASS_B64": b64encode(dnspass), - "HOSTNAME": hostname, - "DNSDOMAIN": dnsdomain, - "REALM": realm, - "HOSTIP_BASE_LINE": hostip_base_line, - "HOSTIP_HOST_LINE": hostip_host_line, - "DOMAINGUID": domainguid, - "DATESTRING": time.strftime("%Y%m%d%H"), - "DEFAULTSITE": DEFAULTSITE, - "NTDSGUID": ntdsguid, - "HOSTIP6_BASE_LINE": hostip6_base_line, - "HOSTIP6_HOST_LINE": hostip6_host_line, - }) - - -def create_named_conf(path, setup_path, realm, dnsdomain, - private_dir): - """Write out a file containing zone statements suitable for inclusion in a - named.conf file (including GSS-TSIG configuration). - - :param path: Path of the new named.conf file. - :param setup_path: Setup path function. - :param realm: Realm name - :param dnsdomain: DNS Domain name - :param private_dir: Path to private directory - :param keytab_name: File name of DNS keytab file - """ - - setup_file(setup_path("named.conf"), path, { - "DNSDOMAIN": dnsdomain, - "REALM": realm, - "REALM_WC": "*." + ".".join(realm.split(".")[1:]), - "PRIVATE_DIR": private_dir - }) - -def create_named_txt(path, setup_path, realm, dnsdomain, - private_dir, keytab_name): - """Write out a file containing zone statements suitable for inclusion in a - named.conf file (including GSS-TSIG configuration). - - :param path: Path of the new named.conf file. - :param setup_path: Setup path function. - :param realm: Realm name - :param dnsdomain: DNS Domain name - :param private_dir: Path to private directory - :param keytab_name: File name of DNS keytab file - """ - - setup_file(setup_path("named.txt"), path, { - "DNSDOMAIN": dnsdomain, - "REALM": realm, - "DNS_KEYTAB": keytab_name, - "DNS_KEYTAB_ABS": os.path.join(private_dir, keytab_name), - "PRIVATE_DIR": private_dir - }) - -def create_krb5_conf(path, setup_path, dnsdomain, hostname, realm): - """Write out a file containing zone statements suitable for inclusion in a - named.conf file (including GSS-TSIG configuration). - - :param path: Path of the new named.conf file. - :param setup_path: Setup path function. - :param dnsdomain: DNS Domain name - :param hostname: Local hostname - :param realm: Realm name - """ - - setup_file(setup_path("krb5.conf"), path, { - "DNSDOMAIN": dnsdomain, - "HOSTNAME": hostname, - "REALM": realm, - }) - - diff --git a/source4/scripting/python/samba/provision/__init__.py b/source4/scripting/python/samba/provision/__init__.py new file mode 100644 index 0000000000..55774c225b --- /dev/null +++ b/source4/scripting/python/samba/provision/__init__.py @@ -0,0 +1,1929 @@ + +# Unix SMB/CIFS implementation. +# backend code for provisioning a Samba4 server + +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2010 +# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2008-2009 +# Copyright (C) Oliver Liebel <oliver@itc.li> 2008-2009 +# +# Based on the original in EJS: +# Copyright (C) Andrew Tridgell <tridge@samba.org> 2005 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +"""Functions for setting up a Samba configuration.""" + +__docformat__ = "restructuredText" + +from base64 import b64encode +import os +import re +import pwd +import grp +import logging +import time +import uuid +import socket +import urllib +import shutil + +import ldb + +from samba.auth import system_session, admin_session +import samba +from samba import ( + Ldb, + check_all_substituted, + in_source_tree, + source_tree_topdir, + read_and_sub_file, + setup_file, + substitute_var, + valid_netbios_name, + version, + ) +from samba.dcerpc import security +from samba.dcerpc.misc import ( + SEC_CHAN_BDC, + SEC_CHAN_WKSTA, + ) +from samba.dsdb import ( + DS_DOMAIN_FUNCTION_2003, + DS_DOMAIN_FUNCTION_2008_R2, + ENC_ALL_TYPES, + ) +from samba.idmap import IDmapDB +from samba.ms_display_specifiers import read_ms_ldif +from samba.ntacls import setntacl, dsacl2fsacl +from samba.ndr import ndr_pack, ndr_unpack +from samba.provision.backend import ( + ExistingBackend, + FDSBackend, + LDBBackend, + OpenLDAPBackend, + ) +import samba.param +import samba.registry +from samba.schema import Schema +from samba.samdb import SamDB + +VALID_NETBIOS_CHARS = " !#$%&'()-.@^_{}~" +DEFAULT_POLICY_GUID = "31B2F340-016D-11D2-945F-00C04FB984F9" +DEFAULT_DC_POLICY_GUID = "6AC1786C-016F-11D2-945F-00C04fB984F9" +DEFAULTSITE = "Default-First-Site-Name" +LAST_PROVISION_USN_ATTRIBUTE = "lastProvisionUSN" + + +def setup_path(file): + """Return an absolute path to the provision tempate file specified by file""" + return os.path.join(samba.param.setup_dir(), file) + +# Descriptors of naming contexts and other important objects + +# "get_schema_descriptor" is located in "schema.py" + +def get_sites_descriptor(domain_sid): + sddl = "D:(A;;RPLCLORC;;;AU)" \ + "(A;;RPWPCRCCLCLORCWOWDSW;;;EA)" \ + "(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)" \ + "S:AI(AU;CISA;CCDCSDDT;;;WD)" \ + "(OU;CIIOSA;CR;;f0f8ffab-1191-11d0-a060-00aa006c33ed;WD)" \ + "(OU;CIIOSA;WP;f30e3bbe-9ff0-11d1-b603-0000f80367c1;bf967ab3-0de6-11d0-a285-00aa003049e2;WD)" \ + "(OU;CIIOSA;WP;f30e3bbf-9ff0-11d1-b603-0000f80367c1;bf967ab3-0de6-11d0-a285-00aa003049e2;WD)" \ + "(OU;CIIOSA;WP;3e10944c-c354-11d0-aff8-0000f80367c1;b7b13124-b82e-11d0-afee-0000f80367c1;WD)" + sec = security.descriptor.from_sddl(sddl, domain_sid) + return 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;;ER)" \ + "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 ndr_pack(sec) + + +def get_domain_descriptor(domain_sid): + sddl= "O:BAG:BAD:AI(OA;CIIO;RP;4c164200-20c0-11d0-a768-00aa006e0529;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)" \ + "(OA;CIIO;RP;4c164200-20c0-11d0-a768-00aa006e0529;bf967aba-0de6-11d0-a285-00aa003049e2;RU)" \ + "(OA;CIIO;RP;5f202010-79a5-11d0-9020-00c04fc2d4cf;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)" \ + "(OA;CIIO;RP;5f202010-79a5-11d0-9020-00c04fc2d4cf;bf967aba-0de6-11d0-a285-00aa003049e2;RU)" \ + "(OA;CIIO;RP;bc0ac240-79a9-11d0-9020-00c04fc2d4cf;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)" \ + "(OA;CIIO;RP;bc0ac240-79a9-11d0-9020-00c04fc2d4cf;bf967aba-0de6-11d0-a285-00aa003049e2;RU)" \ + "(OA;CIIO;RP;59ba2f42-79a2-11d0-9020-00c04fc2d3cf;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)" \ + "(OA;CIIO;RP;59ba2f42-79a2-11d0-9020-00c04fc2d3cf;bf967aba-0de6-11d0-a285-00aa003049e2;RU)" \ + "(OA;CIIO;RP;037088f8-0ae1-11d2-b422-00a0c968f939;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)" \ + "(OA;CIIO;RP;037088f8-0ae1-11d2-b422-00a0c968f939;bf967aba-0de6-11d0-a285-00aa003049e2;RU)" \ + "(OA;;CR;1131f6aa-9c07-11d1-f79f-00c04fc2dcd2;;ER)" \ + "(OA;;CR;1131f6ad-9c07-11d1-f79f-00c04fc2dcd2;;DD)" \ + "(OA;CIIO;RP;b7c69e6d-2cc7-11d2-854e-00a0c983f608;bf967a86-0de6-11d0-a285-00aa003049e2;ED)" \ + "(OA;CIIO;RP;b7c69e6d-2cc7-11d2-854e-00a0c983f608;bf967a9c-0de6-11d0-a285-00aa003049e2;ED)" \ + "(OA;CIIO;RP;b7c69e6d-2cc7-11d2-854e-00a0c983f608;bf967aba-0de6-11d0-a285-00aa003049e2;ED)" \ + "(OA;;CR;89e95b76-444d-4c62-991a-0facbeda640c;;BA)" \ + "(OA;;CR;1131f6aa-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(OA;;CR;1131f6ab-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(OA;;CR;1131f6ac-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(OA;;CR;1131f6ad-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(OA;;CR;1131f6ae-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(OA;;CR;e2a36dc9-ae17-47c3-b58b-be34c55ba633;;IF)" \ + "(OA;;RP;c7407360-20bf-11d0-a768-00aa006e0529;;RU)" \ + "(OA;;RP;b8119fd0-04f6-4762-ab7a-4986c76b3f9a;;RU)" \ + "(OA;CIIO;RPLCLORC;;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)" \ + "(OA;CIIO;RPLCLORC;;bf967a9c-0de6-11d0-a285-00aa003049e2;RU)" \ + "(OA;CIIO;RPLCLORC;;bf967aba-0de6-11d0-a285-00aa003049e2;RU)" \ + "(OA;;CR;05c74c5e-4deb-43b4-bd9f-86664c2a7fd5;;AU)" \ + "(OA;;CR;89e95b76-444d-4c62-991a-0facbeda640c;;ED)" \ + "(OA;;CR;ccc2dc7d-a6ad-4a7a-8846-c04e3cc53501;;AU)" \ + "(OA;;CR;280f369c-67c7-438e-ae98-1d46f3c6f541;;AU)" \ + "(OA;;CR;1131f6aa-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \ + "(OA;;CR;1131f6ab-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \ + "(OA;;CR;1131f6ac-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \ + "(OA;;CR;1131f6ae-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \ + "(OA;;RP;b8119fd0-04f6-4762-ab7a-4986c76b3f9a;;AU)" \ + "(OA;CIIO;RPWPCR;91e647de-d96f-4b70-9557-d63ff4f3ccd8;;PS)" \ + "(A;;RPWPCRCCLCLORCWOWDSW;;;DA)" \ + "(A;CI;RPWPCRCCDCLCLORCWOWDSDDTSW;;;EA)" \ + "(A;;RPRC;;;RU)" \ + "(A;CI;LC;;;RU)" \ + "(A;CI;RPWPCRCCLCLORCWOWDSDSW;;;BA)" \ + "(A;;RP;;;WD)" \ + "(A;;RPLCLORC;;;ED)" \ + "(A;;RPLCLORC;;;AU)" \ + "(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)" \ + "S:AI(OU;CISA;WP;f30e3bbe-9ff0-11d1-b603-0000f80367c1;bf967aa5-0de6-11d0-a285-00aa003049e2;WD)" \ + "(OU;CISA;WP;f30e3bbf-9ff0-11d1-b603-0000f80367c1;bf967aa5-0de6-11d0-a285-00aa003049e2;WD)" \ + "(AU;SA;CR;;;DU)(AU;SA;CR;;;BA)(AU;SA;WPWOWD;;;WD)" + sec = security.descriptor.from_sddl(sddl, domain_sid) + return ndr_pack(sec) + + +class ProvisionPaths(object): + + def __init__(self): + self.shareconf = None + self.hklm = None + self.hkcu = None + self.hkcr = None + self.hku = None + self.hkpd = None + self.hkpt = None + self.samdb = None + self.idmapdb = None + self.secrets = None + self.keytab = None + self.dns_keytab = None + self.dns = None + self.winsdb = None + self.private_dir = None + + +class ProvisionNames(object): + + def __init__(self): + self.rootdn = None + self.domaindn = None + self.configdn = None + self.schemadn = None + self.ldapmanagerdn = None + self.dnsdomain = None + self.realm = None + self.netbiosname = None + self.domain = None + self.hostname = None + self.sitename = None + self.smbconf = None + + +def update_provision_usn(samdb, low, high, replace=False): + """Update the field provisionUSN in sam.ldb + + This field is used to track range of USN modified by provision and + upgradeprovision. + This value is used afterward by next provision to figure out if + the field have been modified since last provision. + + :param samdb: An LDB object connect to sam.ldb + :param low: The lowest USN modified by this upgrade + :param high: The highest USN modified by this upgrade + :param replace: A boolean indicating if the range should replace any + existing one or appended (default) + """ + + tab = [] + if not replace: + entry = samdb.search(expression="(&(dn=@PROVISION)(%s=*))" % + LAST_PROVISION_USN_ATTRIBUTE, base="", + scope=ldb.SCOPE_SUBTREE, + attrs=[LAST_PROVISION_USN_ATTRIBUTE, "dn"]) + for e in entry[0][LAST_PROVISION_USN_ATTRIBUTE]: + tab.append(str(e)) + + tab.append("%s-%s" % (low, high)) + delta = ldb.Message() + delta.dn = ldb.Dn(samdb, "@PROVISION") + delta[LAST_PROVISION_USN_ATTRIBUTE] = ldb.MessageElement(tab, + ldb.FLAG_MOD_REPLACE, LAST_PROVISION_USN_ATTRIBUTE) + samdb.modify(delta) + + +def set_provision_usn(samdb, low, high): + """Set the field provisionUSN in sam.ldb + This field is used to track range of USN modified by provision and + upgradeprovision. + This value is used afterward by next provision to figure out if + the field have been modified since last provision. + + :param samdb: An LDB object connect to sam.ldb + :param low: The lowest USN modified by this upgrade + :param high: The highest USN modified by this upgrade""" + tab = [] + tab.append("%s-%s" % (low, high)) + delta = ldb.Message() + delta.dn = ldb.Dn(samdb, "@PROVISION") + delta[LAST_PROVISION_USN_ATTRIBUTE] = ldb.MessageElement(tab, + ldb.FLAG_MOD_ADD, LAST_PROVISION_USN_ATTRIBUTE) + samdb.add(delta) + + +def get_max_usn(samdb,basedn): + """ This function return the biggest USN present in the provision + + :param samdb: A LDB object pointing to the sam.ldb + :param basedn: A string containing the base DN of the provision + (ie. DC=foo, DC=bar) + :return: The biggest USN in the provision""" + + res = samdb.search(expression="objectClass=*",base=basedn, + scope=ldb.SCOPE_SUBTREE,attrs=["uSNChanged"], + controls=["search_options:1:2", + "server_sort:1:1:uSNChanged", + "paged_results:1:1"]) + return res[0]["uSNChanged"] + + +def get_last_provision_usn(sam): + """Get the lastest USN modified by a provision or an upgradeprovision + + :param sam: An LDB object pointing to the sam.ldb + :return: an integer corresponding to the highest USN modified by + (upgrade)provision, 0 is this value is unknown + """ + entry = sam.search(expression="(&(dn=@PROVISION)(%s=*))" % + LAST_PROVISION_USN_ATTRIBUTE, + base="", scope=ldb.SCOPE_SUBTREE, + attrs=[LAST_PROVISION_USN_ATTRIBUTE]) + if len(entry): + range = [] + idx = 0 + p = re.compile(r'-') + for r in entry[0][LAST_PROVISION_USN_ATTRIBUTE]: + tab = p.split(str(r)) + range.append(tab[0]) + range.append(tab[1]) + idx = idx + 1 + return range + else: + return None + + +class ProvisionResult(object): + + def __init__(self): + self.paths = None + self.domaindn = None + self.lp = None + self.samdb = None + + +def check_install(lp, session_info, credentials): + """Check whether the current install seems ok. + + :param lp: Loadparm context + :param session_info: Session information + :param credentials: Credentials + """ + if lp.get("realm") == "": + raise Exception("Realm empty") + samdb = Ldb(lp.get("sam database"), session_info=session_info, + credentials=credentials, lp=lp) + if len(samdb.search("(cn=Administrator)")) != 1: + raise ProvisioningError("No administrator account found") + + +def findnss(nssfn, names): + """Find a user or group from a list of possibilities. + + :param nssfn: NSS Function to try (should raise KeyError if not found) + :param names: Names to check. + :return: Value return by first names list. + """ + for name in names: + try: + return nssfn(name) + except KeyError: + pass + raise KeyError("Unable to find user/group in %r" % names) + + +findnss_uid = lambda names: findnss(pwd.getpwnam, names)[2] +findnss_gid = lambda names: findnss(grp.getgrnam, names)[2] + + +def setup_add_ldif(ldb, ldif_path, subst_vars=None,controls=["relax:0"]): + """Setup a ldb in the private dir. + + :param ldb: LDB file to import data into + :param ldif_path: Path of the LDIF file to load + :param subst_vars: Optional variables to subsitute in LDIF. + :param nocontrols: Optional list of controls, can be None for no controls + """ + assert isinstance(ldif_path, str) + data = read_and_sub_file(ldif_path, subst_vars) + ldb.add_ldif(data, controls) + + +def setup_modify_ldif(ldb, ldif_path, subst_vars=None,controls=["relax:0"]): + """Modify a ldb in the private dir. + + :param ldb: LDB object. + :param ldif_path: LDIF file path. + :param subst_vars: Optional dictionary with substitution variables. + """ + data = read_and_sub_file(ldif_path, subst_vars) + ldb.modify_ldif(data, controls) + + +def setup_ldb(ldb, ldif_path, subst_vars): + """Import a LDIF a file into a LDB handle, optionally substituting + variables. + + :note: Either all LDIF data will be added or none (using transactions). + + :param ldb: LDB file to import into. + :param ldif_path: Path to the LDIF file. + :param subst_vars: Dictionary with substitution variables. + """ + assert ldb is not None + ldb.transaction_start() + try: + setup_add_ldif(ldb, ldif_path, subst_vars) + except Exception: + ldb.transaction_cancel() + raise + else: + ldb.transaction_commit() + + +def provision_paths_from_lp(lp, dnsdomain): + """Set the default paths for provisioning. + + :param lp: Loadparm context. + :param dnsdomain: DNS Domain name + """ + paths = ProvisionPaths() + paths.private_dir = lp.get("private dir") + + # This is stored without path prefix for the "privateKeytab" attribute in + # "secrets_dns.ldif". + paths.dns_keytab = "dns.keytab" + paths.keytab = "secrets.keytab" + + paths.shareconf = os.path.join(paths.private_dir, "share.ldb") + paths.samdb = os.path.join(paths.private_dir, + 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.privilege = os.path.join(paths.private_dir, "privilege.ldb") + paths.dns = os.path.join(paths.private_dir, "dns", dnsdomain + ".zone") + paths.dns_update_list = os.path.join(paths.private_dir, "dns_update_list") + paths.spn_update_list = os.path.join(paths.private_dir, "spn_update_list") + paths.namedconf = os.path.join(paths.private_dir, "named.conf") + paths.namedconf_update = os.path.join(paths.private_dir, "named.conf.update") + paths.namedtxt = os.path.join(paths.private_dir, "named.txt") + paths.krb5conf = os.path.join(paths.private_dir, "krb5.conf") + paths.winsdb = os.path.join(paths.private_dir, "wins.ldb") + paths.s4_ldapi_path = os.path.join(paths.private_dir, "ldapi") + paths.phpldapadminconfig = os.path.join(paths.private_dir, + "phpldapadmin-config.php") + paths.hklm = "hklm.ldb" + paths.hkcr = "hkcr.ldb" + paths.hkcu = "hkcu.ldb" + paths.hku = "hku.ldb" + paths.hkpd = "hkpd.ldb" + paths.hkpt = "hkpt.ldb" + paths.sysvol = lp.get("path", "sysvol") + paths.netlogon = lp.get("path", "netlogon") + paths.smbconf = lp.configfile + return paths + + +def guess_names(lp=None, hostname=None, domain=None, dnsdomain=None, + serverrole=None, rootdn=None, domaindn=None, configdn=None, + schemadn=None, serverdn=None, sitename=None): + """Guess configuration settings to use.""" + + if hostname is None: + hostname = socket.gethostname().split(".")[0] + + netbiosname = lp.get("netbios name") + if netbiosname is None: + netbiosname = hostname + # remove forbidden chars + newnbname = "" + for x in netbiosname: + if x.isalnum() or x in VALID_NETBIOS_CHARS: + newnbname = "%s%c" % (newnbname, x) + # force the length to be <16 + netbiosname = newnbname[0:15] + assert netbiosname is not None + netbiosname = netbiosname.upper() + if not valid_netbios_name(netbiosname): + raise InvalidNetbiosName(netbiosname) + + if dnsdomain is None: + dnsdomain = lp.get("realm") + if dnsdomain is None or dnsdomain == "": + raise ProvisioningError("guess_names: 'realm' not specified in supplied %s!", lp.configfile) + + dnsdomain = dnsdomain.lower() + + if serverrole is None: + serverrole = lp.get("server role") + if serverrole is None: + raise ProvisioningError("guess_names: 'server role' not specified in supplied %s!" % lp.configfile) + + serverrole = serverrole.lower() + + realm = dnsdomain.upper() + + if lp.get("realm") == "": + raise ProvisioningError("guess_names: 'realm =' was not specified in supplied %s. Please remove the smb.conf file and let provision generate it" % lp.configfile) + + if lp.get("realm").upper() != realm: + raise ProvisioningError("guess_names: 'realm=%s' in %s must match chosen realm '%s'! Please remove the smb.conf file and let provision generate it" % (lp.get("realm").upper(), realm, lp.configfile)) + + if lp.get("server role").lower() != serverrole: + raise ProvisioningError("guess_names: 'server role=%s' in %s must match chosen server role '%s'! Please remove the smb.conf file and let provision generate it" % (lp.get("server role").upper(), serverrole, lp.configfile)) + + if serverrole == "domain controller": + if domain is None: + # This will, for better or worse, default to 'WORKGROUP' + domain = lp.get("workgroup") + domain = domain.upper() + + if lp.get("workgroup").upper() != domain: + raise ProvisioningError("guess_names: Workgroup '%s' in smb.conf must match chosen domain '%s'! Please remove the %s file and let provision generate it" % (lp.get("workgroup").upper(), domain, lp.configfile)) + + if domaindn is None: + domaindn = "DC=" + dnsdomain.replace(".", ",DC=") + + if domain == netbiosname: + raise ProvisioningError("guess_names: Domain '%s' must not be equal to short host name '%s'!" % (domain, netbiosname)) + else: + domain = netbiosname + if domaindn is None: + domaindn = "DC=" + netbiosname + + if not valid_netbios_name(domain): + raise InvalidNetbiosName(domain) + + if hostname.upper() == realm: + raise ProvisioningError("guess_names: Realm '%s' must not be equal to hostname '%s'!" % (realm, hostname)) + if netbiosname.upper() == realm: + raise ProvisioningError("guess_names: Realm '%s' must not be equal to netbios hostname '%s'!" % (realm, netbiosname)) + if domain == realm: + raise ProvisioningError("guess_names: Realm '%s' must not be equal to short domain name '%s'!" % (realm, domain)) + + if rootdn is None: + rootdn = domaindn + + if configdn is None: + configdn = "CN=Configuration," + rootdn + if schemadn is None: + schemadn = "CN=Schema," + configdn + + if sitename is None: + sitename=DEFAULTSITE + + names = ProvisionNames() + names.rootdn = rootdn + names.domaindn = domaindn + names.configdn = configdn + names.schemadn = schemadn + names.ldapmanagerdn = "CN=Manager," + rootdn + names.dnsdomain = dnsdomain + names.domain = domain + names.realm = realm + names.netbiosname = netbiosname + names.hostname = hostname + names.sitename = sitename + names.serverdn = "CN=%s,CN=Servers,CN=%s,CN=Sites,%s" % ( + netbiosname, sitename, configdn) + + return names + + +def make_smbconf(smbconf, hostname, domain, realm, serverrole, + targetdir, sid_generator="internal", eadb=False, lp=None): + """Create a new smb.conf file based on a couple of basic settings. + """ + assert smbconf is not None + if hostname is None: + hostname = socket.gethostname().split(".")[0] + netbiosname = hostname.upper() + # remove forbidden chars + newnbname = "" + for x in netbiosname: + if x.isalnum() or x in VALID_NETBIOS_CHARS: + newnbname = "%s%c" % (newnbname, x) + #force the length to be <16 + netbiosname = newnbname[0:15] + else: + netbiosname = hostname.upper() + + if serverrole is None: + serverrole = "standalone" + + assert serverrole in ("domain controller", "member server", "standalone") + if serverrole == "domain controller": + smbconfsuffix = "dc" + elif serverrole == "member server": + smbconfsuffix = "member" + elif serverrole == "standalone": + smbconfsuffix = "standalone" + + if sid_generator is None: + sid_generator = "internal" + + assert domain is not None + domain = domain.upper() + + assert realm is not None + realm = realm.upper() + + if lp is None: + lp = samba.param.LoadParm() + #Load non-existant file + if os.path.exists(smbconf): + lp.load(smbconf) + if eadb and not lp.get("posix:eadb"): + if targetdir is not None: + privdir = os.path.join(targetdir, "private") + else: + privdir = lp.get("private dir") + lp.set("posix:eadb", os.path.abspath(os.path.join(privdir, "eadb.tdb"))) + + if targetdir is not None: + privatedir_line = "private dir = " + os.path.abspath(os.path.join(targetdir, "private")) + lockdir_line = "lock dir = " + os.path.abspath(targetdir) + + lp.set("lock dir", os.path.abspath(targetdir)) + else: + privatedir_line = "" + lockdir_line = "" + + if sid_generator == "internal": + sid_generator_line = "" + else: + sid_generator_line = "sid generator = " + sid_generator + + sysvol = os.path.join(lp.get("lock dir"), "sysvol") + netlogon = os.path.join(sysvol, realm.lower(), "scripts") + + setup_file(setup_path("provision.smb.conf.%s" % smbconfsuffix), + smbconf, { + "NETBIOS_NAME": netbiosname, + "DOMAIN": domain, + "REALM": realm, + "SERVERROLE": serverrole, + "NETLOGONPATH": netlogon, + "SYSVOLPATH": sysvol, + "SIDGENERATOR_LINE": sid_generator_line, + "PRIVATEDIR_LINE": privatedir_line, + "LOCKDIR_LINE": lockdir_line + }) + + # reload the smb.conf + lp.load(smbconf) + + # and dump it without any values that are the default + # this ensures that any smb.conf parameters that were set + # on the provision/join command line are set in the resulting smb.conf + f = open(smbconf, mode='w') + lp.dump(f, False) + f.close() + + + +def setup_name_mappings(samdb, idmap, sid, domaindn, root_uid, nobody_uid, + users_gid, wheel_gid): + """setup reasonable name mappings for sam names to unix names. + + :param samdb: SamDB object. + :param idmap: IDmap db object. + :param sid: The domain sid. + :param domaindn: The domain DN. + :param root_uid: uid of the UNIX root user. + :param nobody_uid: uid of the UNIX nobody user. + :param users_gid: gid of the UNIX users group. + :param wheel_gid: gid of the UNIX wheel group. + """ + 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, logger, lp, session_info, + provision_backend, names, schema, serverrole, + erase=False): + """Setup the partitions for the SAM database. + + Alternatively, provision() may call this, and then populate the database. + + :note: This will wipe the Sam Database! + + :note: This function always removes the local SAM LDB file. The erase + parameter controls whether to erase the existing data, which + may not be stored locally but in LDAP. + + """ + assert session_info is not None + + # We use options=["modules:"] to stop the modules loading - we + # just want to wipe and re-initialise the database, not start it up + + try: + os.unlink(samdb_path) + except OSError: + pass + + samdb = Ldb(url=samdb_path, session_info=session_info, + lp=lp, options=["modules:"]) + + ldap_backend_line = "# No LDAP backend" + if provision_backend.type is not "ldb": + ldap_backend_line = "ldapBackend: %s" % provision_backend.ldap_uri + + samdb.transaction_start() + try: + logger.info("Setting up sam.ldb partitions and settings") + setup_add_ldif(samdb, setup_path("provision_partitions.ldif"), { + "SCHEMADN": ldb.Dn(schema.ldb, names.schemadn).get_casefold(), + "CONFIGDN": ldb.Dn(schema.ldb, names.configdn).get_casefold(), + "DOMAINDN": ldb.Dn(schema.ldb, names.domaindn).get_casefold(), + "LDAP_BACKEND_LINE": ldap_backend_line, + }) + + + setup_add_ldif(samdb, setup_path("provision_init.ldif"), { + "BACKEND_TYPE": provision_backend.type, + "SERVER_ROLE": serverrole + }) + + logger.info("Setting up sam.ldb rootDSE") + setup_samdb_rootdse(samdb, names) + except Exception: + samdb.transaction_cancel() + raise + else: + samdb.transaction_commit() + + +def secretsdb_self_join(secretsdb, domain, + netbiosname, machinepass, domainsid=None, + realm=None, dnsdomain=None, + keytab_path=None, + key_version_number=1, + secure_channel_type=SEC_CHAN_WKSTA): + """Add domain join-specific bits to a secrets database. + + :param secretsdb: Ldb Handle to the secrets database + :param machinepass: Machine password + """ + attrs = ["whenChanged", + "secret", + "priorSecret", + "priorChanged", + "krb5Keytab", + "privateKeytab"] + + if realm is not None: + if dnsdomain is None: + dnsdomain = realm.lower() + dnsname = '%s.%s' % (netbiosname.lower(), dnsdomain.lower()) + else: + dnsname = None + shortname = netbiosname.lower() + + # We don't need to set msg["flatname"] here, because rdn_name will handle + # it, and it causes problems for modifies anyway + msg = ldb.Message(ldb.Dn(secretsdb, "flatname=%s,cn=Primary Domains" % domain)) + msg["secureChannelType"] = [str(secure_channel_type)] + msg["objectClass"] = ["top", "primaryDomain"] + if dnsname is not None: + msg["objectClass"] = ["top", "primaryDomain", "kerberosSecret"] + msg["realm"] = [realm] + msg["saltPrincipal"] = ["host/%s@%s" % (dnsname, realm.upper())] + msg["msDS-KeyVersionNumber"] = [str(key_version_number)] + msg["privateKeytab"] = ["secrets.keytab"] + + msg["secret"] = [machinepass] + msg["samAccountName"] = ["%s$" % netbiosname] + msg["secureChannelType"] = [str(secure_channel_type)] + if domainsid is not None: + msg["objectSid"] = [ndr_pack(domainsid)] + + # This complex expression tries to ensure that we don't have more + # than one record for this SID, realm or netbios domain at a time, + # but we don't delete the old record that we are about to modify, + # because that would delete the keytab and previous password. + res = secretsdb.search(base="cn=Primary Domains", attrs=attrs, + expression=("(&(|(flatname=%s)(realm=%s)(objectSid=%s))(objectclass=primaryDomain)(!(dn=%s)))" % (domain, realm, str(domainsid), str(msg.dn))), + scope=ldb.SCOPE_ONELEVEL) + + for del_msg in res: + secretsdb.delete(del_msg.dn) + + res = secretsdb.search(base=msg.dn, attrs=attrs, scope=ldb.SCOPE_BASE) + + if len(res) == 1: + msg["priorSecret"] = [res[0]["secret"][0]] + msg["priorWhenChanged"] = [res[0]["whenChanged"][0]] + + try: + msg["privateKeytab"] = [res[0]["privateKeytab"][0]] + except KeyError: + pass + + try: + msg["krb5Keytab"] = [res[0]["krb5Keytab"][0]] + except KeyError: + pass + + for el in msg: + if el != 'dn': + msg[el].set_flags(ldb.FLAG_MOD_REPLACE) + secretsdb.modify(msg) + secretsdb.rename(res[0].dn, msg.dn) + else: + spn = [ 'HOST/%s' % shortname ] + if secure_channel_type == SEC_CHAN_BDC and dnsname is not None: + # we are a domain controller then we add servicePrincipalName + # entries for the keytab code to update. + spn.extend([ 'HOST/%s' % dnsname ]) + msg["servicePrincipalName"] = spn + + secretsdb.add(msg) + + +def secretsdb_setup_dns(secretsdb, names, private_dir, realm, + dnsdomain, dns_keytab_path, dnspass): + """Add DNS specific bits to a secrets database. + + :param secretsdb: Ldb Handle to the secrets database + :param machinepass: Machine password + """ + try: + os.unlink(os.path.join(private_dir, dns_keytab_path)) + except OSError: + pass + + setup_ldb(secretsdb, setup_path("secrets_dns.ldif"), { + "REALM": realm, + "DNSDOMAIN": dnsdomain, + "DNS_KEYTAB": dns_keytab_path, + "DNSPASS_B64": b64encode(dnspass), + "HOSTNAME": names.hostname, + "DNSNAME" : '%s.%s' % ( + names.netbiosname.lower(), names.dnsdomain.lower()) + }) + + +def setup_secretsdb(paths, session_info, backend_credentials, lp): + """Setup the secrets database. + + :note: This function does not handle exceptions and transaction on purpose, + it's up to the caller to do this job. + + :param path: Path to the secrets database. + :param session_info: Session info. + :param credentials: Credentials + :param lp: Loadparm context + :return: LDB handle for the created secrets database + """ + if os.path.exists(paths.secrets): + os.unlink(paths.secrets) + + keytab_path = os.path.join(paths.private_dir, paths.keytab) + if os.path.exists(keytab_path): + os.unlink(keytab_path) + + dns_keytab_path = os.path.join(paths.private_dir, paths.dns_keytab) + if os.path.exists(dns_keytab_path): + os.unlink(dns_keytab_path) + + path = paths.secrets + + secrets_ldb = Ldb(path, session_info=session_info, + lp=lp) + secrets_ldb.erase() + secrets_ldb.load_ldif_file_add(setup_path("secrets_init.ldif")) + secrets_ldb = Ldb(path, session_info=session_info, + lp=lp) + secrets_ldb.transaction_start() + try: + secrets_ldb.load_ldif_file_add(setup_path("secrets.ldif")) + + if (backend_credentials is not None and + backend_credentials.authentication_requested()): + if backend_credentials.get_bind_dn() is not None: + setup_add_ldif(secrets_ldb, + setup_path("secrets_simple_ldap.ldif"), { + "LDAPMANAGERDN": backend_credentials.get_bind_dn(), + "LDAPMANAGERPASS_B64": b64encode(backend_credentials.get_password()) + }) + else: + setup_add_ldif(secrets_ldb, + setup_path("secrets_sasl_ldap.ldif"), { + "LDAPADMINUSER": backend_credentials.get_username(), + "LDAPADMINREALM": backend_credentials.get_realm(), + "LDAPADMINPASS_B64": b64encode(backend_credentials.get_password()) + }) + + return secrets_ldb + except Exception: + secrets_ldb.transaction_cancel() + raise + + +def setup_privileges(path, session_info, lp): + """Setup the privileges database. + + :param path: Path to the privileges database. + :param session_info: Session info. + :param credentials: Credentials + :param lp: Loadparm context + :return: LDB handle for the created secrets database + """ + if os.path.exists(path): + os.unlink(path) + privilege_ldb = Ldb(path, session_info=session_info, lp=lp) + privilege_ldb.erase() + privilege_ldb.load_ldif_file_add(setup_path("provision_privilege.ldif")) + + +def setup_registry(path, session_info, lp): + """Setup the registry. + + :param path: Path to the registry database + :param session_info: Session information + :param credentials: Credentials + :param lp: Loadparm context + """ + reg = samba.registry.Registry() + hive = samba.registry.open_ldb(path, session_info=session_info, lp_ctx=lp) + reg.mount_hive(hive, samba.registry.HKEY_LOCAL_MACHINE) + provision_reg = setup_path("provision.reg") + assert os.path.exists(provision_reg) + reg.diff_apply(provision_reg) + + +def setup_idmapdb(path, session_info, lp): + """Setup the idmap database. + + :param path: path to the idmap database + :param session_info: Session information + :param credentials: Credentials + :param lp: Loadparm context + """ + if os.path.exists(path): + os.unlink(path) + + idmap_ldb = IDmapDB(path, session_info=session_info, lp=lp) + idmap_ldb.erase() + idmap_ldb.load_ldif_file_add(setup_path("idmap_init.ldif")) + return idmap_ldb + + +def setup_samdb_rootdse(samdb, names): + """Setup the SamDB rootdse. + + :param samdb: Sam Database handle + """ + setup_add_ldif(samdb, setup_path("provision_rootdse_add.ldif"), { + "SCHEMADN": names.schemadn, + "DOMAINDN": names.domaindn, + "ROOTDN": names.rootdn, + "CONFIGDN": names.configdn, + "SERVERDN": names.serverdn, + }) + + +def setup_self_join(samdb, names, machinepass, dnspass, + domainsid, next_rid, invocationid, + policyguid, policyguid_dc, domainControllerFunctionality, + ntdsguid): + """Join a host to its own domain.""" + assert isinstance(invocationid, str) + if ntdsguid is not None: + ntdsguid_line = "objectGUID: %s\n"%ntdsguid + else: + ntdsguid_line = "" + setup_add_ldif(samdb, setup_path("provision_self_join.ldif"), { + "CONFIGDN": names.configdn, + "SCHEMADN": names.schemadn, + "DOMAINDN": names.domaindn, + "SERVERDN": names.serverdn, + "INVOCATIONID": invocationid, + "NETBIOSNAME": names.netbiosname, + "DNSNAME": "%s.%s" % (names.hostname, names.dnsdomain), + "MACHINEPASS_B64": b64encode(machinepass.encode('utf-16-le')), + "DOMAINSID": str(domainsid), + "DCRID": str(next_rid), + "SAMBA_VERSION_STRING": version, + "NTDSGUID": ntdsguid_line, + "DOMAIN_CONTROLLER_FUNCTIONALITY": str( + domainControllerFunctionality)}) + + setup_add_ldif(samdb, setup_path("provision_group_policy.ldif"), { + "POLICYGUID": policyguid, + "POLICYGUID_DC": policyguid_dc, + "DNSDOMAIN": names.dnsdomain, + "DOMAINDN": names.domaindn}) + + # add the NTDSGUID based SPNs + ntds_dn = "CN=NTDS Settings,%s" % names.serverdn + names.ntdsguid = samdb.searchone(basedn=ntds_dn, attribute="objectGUID", + expression="", scope=ldb.SCOPE_BASE) + assert isinstance(names.ntdsguid, str) + + # Setup fSMORoleOwner entries to point at the newly created DC entry + setup_modify_ldif(samdb, setup_path("provision_self_join_modify.ldif"), { + "DOMAINDN": names.domaindn, + "CONFIGDN": names.configdn, + "SCHEMADN": names.schemadn, + "DEFAULTSITE": names.sitename, + "SERVERDN": names.serverdn, + "NETBIOSNAME": names.netbiosname, + "RIDALLOCATIONSTART": str(next_rid + 100), + "RIDALLOCATIONEND": str(next_rid + 100 + 499), + }) + + # This is partially Samba4 specific and should be replaced by the correct + # DNS AD-style setup + setup_add_ldif(samdb, setup_path("provision_dns_add.ldif"), { + "DNSDOMAIN": names.dnsdomain, + "DOMAINDN": names.domaindn, + "DNSPASS_B64": b64encode(dnspass.encode('utf-16-le')), + "HOSTNAME" : names.hostname, + "DNSNAME" : '%s.%s' % ( + names.netbiosname.lower(), names.dnsdomain.lower()) + }) + + +def getpolicypath(sysvolpath, dnsdomain, guid): + """Return the physical path of policy given its guid. + + :param sysvolpath: Path to the sysvol folder + :param dnsdomain: DNS name of the AD domain + :param guid: The GUID of the policy + :return: A string with the complete path to the policy folder + """ + + if guid[0] != "{": + guid = "{%s}" % guid + policy_path = os.path.join(sysvolpath, dnsdomain, "Policies", guid) + return policy_path + + +def create_gpo_struct(policy_path): + if not os.path.exists(policy_path): + os.makedirs(policy_path, 0775) + open(os.path.join(policy_path, "GPT.INI"), 'w').write( + "[General]\r\nVersion=0") + p = os.path.join(policy_path, "MACHINE") + if not os.path.exists(p): + os.makedirs(p, 0775) + p = os.path.join(policy_path, "USER") + if not os.path.exists(p): + os.makedirs(p, 0775) + + +def create_default_gpo(sysvolpath, dnsdomain, policyguid, policyguid_dc): + """Create the default GPO for a domain + + :param sysvolpath: Physical path for the sysvol folder + :param dnsdomain: DNS domain name of the AD domain + :param policyguid: GUID of the default domain policy + :param policyguid_dc: GUID of the default domain controler policy + """ + policy_path = getpolicypath(sysvolpath,dnsdomain,policyguid) + create_gpo_struct(policy_path) + + policy_path = getpolicypath(sysvolpath,dnsdomain,policyguid_dc) + create_gpo_struct(policy_path) + + +def setup_samdb(path, session_info, provision_backend, lp, names, + logger, domainsid, domainguid, policyguid, policyguid_dc, fill, + adminpass, krbtgtpass, machinepass, invocationid, dnspass, ntdsguid, + serverrole, am_rodc=False, dom_for_fun_level=None, schema=None, + next_rid=1000): + """Setup a complete SAM Database. + + :note: This will wipe the main SAM database file! + """ + + # Provision does not make much sense values larger than 1000000000 + # as the upper range of the rIDAvailablePool is 1073741823 and + # we don't want to create a domain that cannot allocate rids. + if next_rid < 1000 or next_rid > 1000000000: + error = "You want to run SAMBA 4 with a next_rid of %u, " % (next_rid) + error += "the valid range is %u-%u. The default is %u." % ( + 1000, 1000000000, 1000) + raise ProvisioningError(error) + + # ATTENTION: Do NOT change these default values without discussion with the + # team and/or release manager. They have a big impact on the whole program! + domainControllerFunctionality = DS_DOMAIN_FUNCTION_2008_R2 + + if dom_for_fun_level is None: + dom_for_fun_level = DS_DOMAIN_FUNCTION_2003 + + if dom_for_fun_level > domainControllerFunctionality: + raise ProvisioningError("You want to run SAMBA 4 on a domain and forest function level which itself is higher than its actual DC function level (2008_R2). This won't work!") + + domainFunctionality = dom_for_fun_level + forestFunctionality = dom_for_fun_level + + # Also wipes the database + setup_samdb_partitions(path, logger=logger, lp=lp, + provision_backend=provision_backend, session_info=session_info, + names=names, serverrole=serverrole, schema=schema) + + if schema is None: + schema = Schema(domainsid, schemadn=names.schemadn) + + # Load the database, but don's load the global schema and don't connect + # quite yet + samdb = SamDB(session_info=session_info, url=None, auto_connect=False, + credentials=provision_backend.credentials, lp=lp, + global_schema=False, am_rodc=am_rodc) + + logger.info("Pre-loading the Samba 4 and AD schema") + + # Load the schema from the one we computed earlier + samdb.set_schema(schema) + + # Set the NTDS settings DN manually - in order to have it already around + # before the provisioned tree exists and we connect + samdb.set_ntds_settings_dn("CN=NTDS Settings,%s" % names.serverdn) + + # And now we can connect to the DB - the schema won't be loaded from the + # DB + samdb.connect(path) + + if fill == FILL_DRS: + return samdb + + samdb.transaction_start() + try: + # Set the domain functionality levels onto the database. + # Various module (the password_hash module in particular) need + # to know what level of AD we are emulating. + + # These will be fixed into the database via the database + # modifictions below, but we need them set from the start. + samdb.set_opaque_integer("domainFunctionality", domainFunctionality) + samdb.set_opaque_integer("forestFunctionality", forestFunctionality) + samdb.set_opaque_integer("domainControllerFunctionality", + domainControllerFunctionality) + + samdb.set_domain_sid(str(domainsid)) + samdb.set_invocation_id(invocationid) + + logger.info("Adding DomainDN: %s" % names.domaindn) + + # impersonate domain admin + admin_session_info = admin_session(lp, str(domainsid)) + samdb.set_session_info(admin_session_info) + if domainguid is not None: + domainguid_line = "objectGUID: %s\n-" % domainguid + else: + domainguid_line = "" + + descr = b64encode(get_domain_descriptor(domainsid)) + setup_add_ldif(samdb, setup_path("provision_basedn.ldif"), { + "DOMAINDN": names.domaindn, + "DOMAINSID": str(domainsid), + "DESCRIPTOR": descr, + "DOMAINGUID": domainguid_line + }) + + setup_modify_ldif(samdb, setup_path("provision_basedn_modify.ldif"), { + "DOMAINDN": names.domaindn, + "CREATTIME": str(int(time.time() * 1e7)), # seconds -> ticks + "NEXTRID": str(next_rid), + "DEFAULTSITE": names.sitename, + "CONFIGDN": names.configdn, + "POLICYGUID": policyguid, + "DOMAIN_FUNCTIONALITY": str(domainFunctionality), + "SAMBA_VERSION_STRING": version + }) + + logger.info("Adding configuration container") + descr = b64encode(get_config_descriptor(domainsid)) + setup_add_ldif(samdb, setup_path("provision_configuration_basedn.ldif"), { + "CONFIGDN": names.configdn, + "DESCRIPTOR": descr, + }) + + # The LDIF here was created when the Schema object was constructed + logger.info("Setting up sam.ldb schema") + samdb.add_ldif(schema.schema_dn_add, controls=["relax:0"]) + samdb.modify_ldif(schema.schema_dn_modify) + samdb.write_prefixes_from_schema() + samdb.add_ldif(schema.schema_data, controls=["relax:0"]) + setup_add_ldif(samdb, setup_path("aggregate_schema.ldif"), + {"SCHEMADN": names.schemadn}) + + logger.info("Reopening sam.ldb with new schema") + except Exception: + samdb.transaction_cancel() + raise + else: + samdb.transaction_commit() + + samdb = SamDB(session_info=admin_session_info, auto_connect=False, + credentials=provision_backend.credentials, lp=lp, + global_schema=False, am_rodc=am_rodc) + + # Set the NTDS settings DN manually - in order to have it already around + # before the provisioned tree exists and we connect + samdb.set_ntds_settings_dn("CN=NTDS Settings,%s" % names.serverdn) + samdb.connect(path) + + samdb.transaction_start() + try: + samdb.invocation_id = invocationid + + logger.info("Setting up sam.ldb configuration data") + descr = b64encode(get_sites_descriptor(domainsid)) + setup_add_ldif(samdb, setup_path("provision_configuration.ldif"), { + "CONFIGDN": names.configdn, + "NETBIOSNAME": names.netbiosname, + "DEFAULTSITE": names.sitename, + "DNSDOMAIN": names.dnsdomain, + "DOMAIN": names.domain, + "SCHEMADN": names.schemadn, + "DOMAINDN": names.domaindn, + "SERVERDN": names.serverdn, + "FOREST_FUNCTIONALITY": str(forestFunctionality), + "DOMAIN_FUNCTIONALITY": str(domainFunctionality), + "SITES_DESCRIPTOR": descr + }) + + logger.info("Setting up display specifiers") + display_specifiers_ldif = read_ms_ldif( + setup_path('display-specifiers/DisplaySpecifiers-Win2k8R2.txt')) + display_specifiers_ldif = substitute_var(display_specifiers_ldif, + {"CONFIGDN": names.configdn}) + check_all_substituted(display_specifiers_ldif) + samdb.add_ldif(display_specifiers_ldif) + + logger.info("Adding users container") + setup_add_ldif(samdb, setup_path("provision_users_add.ldif"), { + "DOMAINDN": names.domaindn}) + logger.info("Modifying users container") + setup_modify_ldif(samdb, setup_path("provision_users_modify.ldif"), { + "DOMAINDN": names.domaindn}) + logger.info("Adding computers container") + setup_add_ldif(samdb, setup_path("provision_computers_add.ldif"), { + "DOMAINDN": names.domaindn}) + logger.info("Modifying computers container") + setup_modify_ldif(samdb, + setup_path("provision_computers_modify.ldif"), { + "DOMAINDN": names.domaindn}) + logger.info("Setting up sam.ldb data") + 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, + "RIDAVAILABLESTART": str(next_rid + 600), + "POLICYGUID_DC": policyguid_dc + }) + + setup_modify_ldif(samdb, + setup_path("provision_basedn_references.ldif"), { + "DOMAINDN": names.domaindn}) + + setup_modify_ldif(samdb, + setup_path("provision_configuration_references.ldif"), { + "CONFIGDN": names.configdn, + "SCHEMADN": names.schemadn}) + if fill == FILL_FULL: + logger.info("Setting up sam.ldb users and groups") + setup_add_ldif(samdb, setup_path("provision_users.ldif"), { + "DOMAINDN": names.domaindn, + "DOMAINSID": str(domainsid), + "CONFIGDN": names.configdn, + "ADMINPASS_B64": b64encode(adminpass.encode('utf-16-le')), + "KRBTGTPASS_B64": b64encode(krbtgtpass.encode('utf-16-le')) + }) + + logger.info("Setting up self join") + setup_self_join(samdb, names=names, invocationid=invocationid, + dnspass=dnspass, + machinepass=machinepass, + domainsid=domainsid, + next_rid=next_rid, + policyguid=policyguid, + policyguid_dc=policyguid_dc, + domainControllerFunctionality=domainControllerFunctionality, + ntdsguid=ntdsguid) + + ntds_dn = "CN=NTDS Settings,%s" % names.serverdn + names.ntdsguid = samdb.searchone(basedn=ntds_dn, + attribute="objectGUID", expression="", scope=ldb.SCOPE_BASE) + assert isinstance(names.ntdsguid, str) + except Exception: + samdb.transaction_cancel() + raise + else: + samdb.transaction_commit() + return samdb + + +FILL_FULL = "FULL" +FILL_NT4SYNC = "NT4SYNC" +FILL_DRS = "DRS" +SYSVOL_ACL = "O:LAG:BAD:P(A;OICI;0x001f01ff;;;BA)(A;OICI;0x001200a9;;;SO)(A;OICI;0x001f01ff;;;SY)(A;OICI;0x001200a9;;;AU)" +POLICIES_ACL = "O:LAG:BAD:P(A;OICI;0x001f01ff;;;BA)(A;OICI;0x001200a9;;;SO)(A;OICI;0x001f01ff;;;SY)(A;OICI;0x001200a9;;;AU)(A;OICI;0x001301bf;;;PA)" + + +def set_dir_acl(path, acl, lp, domsid): + setntacl(lp, path, acl, domsid) + for root, dirs, files in os.walk(path, topdown=False): + for name in files: + setntacl(lp, os.path.join(root, name), acl, domsid) + for name in dirs: + setntacl(lp, os.path.join(root, name), acl, domsid) + + +def set_gpos_acl(sysvol, dnsdomain, domainsid, domaindn, samdb, lp): + """Set ACL on the sysvol/<dnsname>/Policies folder and the policy + folders beneath. + + :param sysvol: Physical path for the sysvol folder + :param dnsdomain: The DNS name of the domain + :param domainsid: The SID of the domain + :param domaindn: The DN of the domain (ie. DC=...) + :param samdb: An LDB object on the SAM db + :param lp: an LP object + """ + + # Set ACL for GPO root folder + root_policy_path = os.path.join(sysvol, dnsdomain, "Policies") + setntacl(lp, root_policy_path, POLICIES_ACL, str(domainsid)) + + res = samdb.search(base="CN=Policies,CN=System,%s"%(domaindn), + attrs=["cn", "nTSecurityDescriptor"], + expression="", scope=ldb.SCOPE_ONELEVEL) + + for policy in res: + acl = ndr_unpack(security.descriptor, + str(policy["nTSecurityDescriptor"])).as_sddl() + policy_path = getpolicypath(sysvol, dnsdomain, str(policy["cn"])) + set_dir_acl(policy_path, dsacl2fsacl(acl, str(domainsid)), lp, + str(domainsid)) + + +def setsysvolacl(samdb, netlogon, sysvol, gid, domainsid, dnsdomain, domaindn, + lp): + """Set the ACL for the sysvol share and the subfolders + + :param samdb: An LDB object on the SAM db + :param netlogon: Physical path for the netlogon folder + :param sysvol: Physical path for the sysvol folder + :param gid: The GID of the "Domain adminstrators" group + :param domainsid: The SID of the domain + :param dnsdomain: The DNS name of the domain + :param domaindn: The DN of the domain (ie. DC=...) + """ + + try: + os.chown(sysvol, -1, gid) + except OSError: + canchown = False + else: + canchown = True + + # Set the SYSVOL_ACL on the sysvol folder and subfolder (first level) + setntacl(lp,sysvol, SYSVOL_ACL, str(domainsid)) + for root, dirs, files in os.walk(sysvol, topdown=False): + for name in files: + if canchown: + os.chown(os.path.join(root, name), -1, gid) + setntacl(lp, os.path.join(root, name), SYSVOL_ACL, str(domainsid)) + for name in dirs: + if canchown: + os.chown(os.path.join(root, name), -1, gid) + setntacl(lp, os.path.join(root, name), SYSVOL_ACL, str(domainsid)) + + # Set acls on Policy folder and policies folders + set_gpos_acl(sysvol, dnsdomain, domainsid, domaindn, samdb, lp) + + +def provision(logger, session_info, credentials, smbconf=None, + targetdir=None, samdb_fill=FILL_FULL, realm=None, rootdn=None, + domaindn=None, schemadn=None, configdn=None, serverdn=None, + domain=None, hostname=None, hostip=None, hostip6=None, domainsid=None, + next_rid=1000, adminpass=None, ldapadminpass=None, krbtgtpass=None, + domainguid=None, policyguid=None, policyguid_dc=None, + invocationid=None, machinepass=None, ntdsguid=None, dnspass=None, + root=None, nobody=None, users=None, wheel=None, backup=None, aci=None, + serverrole=None, dom_for_fun_level=None, ldap_backend_extra_port=None, + ldap_backend_forced_uri=None, 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, useeadb=False, am_rodc=False, + lp=None): + """Provision samba4 + + :note: caution, this wipes all existing data! + """ + + if domainsid is None: + domainsid = security.random_sid() + else: + domainsid = security.dom_sid(domainsid) + + # create/adapt the group policy GUIDs + # Default GUID for default policy are described at + # "How Core Group Policy Works" + # http://technet.microsoft.com/en-us/library/cc784268%28WS.10%29.aspx + if policyguid is None: + policyguid = DEFAULT_POLICY_GUID + policyguid = policyguid.upper() + if policyguid_dc is None: + policyguid_dc = DEFAULT_DC_POLICY_GUID + policyguid_dc = policyguid_dc.upper() + + if adminpass is None: + adminpass = samba.generate_random_password(12, 32) + if krbtgtpass is None: + krbtgtpass = samba.generate_random_password(128, 255) + if machinepass is None: + machinepass = samba.generate_random_password(128, 255) + if dnspass is None: + dnspass = samba.generate_random_password(128, 255) + if ldapadminpass is None: + # Make a new, random password between Samba and it's LDAP server + ldapadminpass=samba.generate_random_password(128, 255) + + if backend_type is None: + backend_type = "ldb" + + sid_generator = "internal" + if backend_type == "fedora-ds": + sid_generator = "backend" + + root_uid = findnss_uid([root or "root"]) + nobody_uid = findnss_uid([nobody or "nobody"]) + users_gid = findnss_gid([users or "users", 'users', 'other', 'staff']) + if wheel is None: + wheel_gid = findnss_gid(["wheel", "adm"]) + else: + wheel_gid = findnss_gid([wheel]) + try: + bind_gid = findnss_gid(["bind", "named"]) + except KeyError: + bind_gid = None + + if targetdir is not None: + smbconf = os.path.join(targetdir, "etc", "smb.conf") + elif smbconf is None: + smbconf = samba.param.default_path() + if not os.path.exists(os.path.dirname(smbconf)): + os.makedirs(os.path.dirname(smbconf)) + + # only install a new smb.conf if there isn't one there already + if os.path.exists(smbconf): + # if Samba Team members can't figure out the weird errors + # loading an empty smb.conf gives, then we need to be smarter. + # Pretend it just didn't exist --abartlet + data = open(smbconf, 'r').read() + data = data.lstrip() + if data is None or data == "": + make_smbconf(smbconf, hostname, domain, realm, + serverrole, targetdir, sid_generator, useeadb, + lp=lp) + else: + make_smbconf(smbconf, hostname, domain, realm, serverrole, + targetdir, sid_generator, useeadb, lp=lp) + + if lp is None: + lp = samba.param.LoadParm() + lp.load(smbconf) + names = guess_names(lp=lp, hostname=hostname, domain=domain, + dnsdomain=realm, serverrole=serverrole, domaindn=domaindn, + configdn=configdn, schemadn=schemadn, serverdn=serverdn, + sitename=sitename) + paths = provision_paths_from_lp(lp, names.dnsdomain) + + paths.bind_gid = bind_gid + + if hostip is None: + logger.info("Looking up IPv4 addresses") + hostips = samba.interface_ips(lp, False) + if len(hostips) == 0: + logger.warning("No external IPv4 address has been found. Using loopback.") + hostip = '127.0.0.1' + else: + hostip = hostips[0] + if len(hostips) > 1: + logger.warning("More than one IPv4 address found. Using %s.", + hostip) + + if serverrole is None: + serverrole = lp.get("server role") + + assert serverrole in ("domain controller", "member server", "standalone") + if invocationid is None: + invocationid = str(uuid.uuid4()) + + if not os.path.exists(paths.private_dir): + os.mkdir(paths.private_dir) + if not os.path.exists(os.path.join(paths.private_dir, "tls")): + os.mkdir(os.path.join(paths.private_dir, "tls")) + + ldapi_url = "ldapi://%s" % urllib.quote(paths.s4_ldapi_path, safe="") + + schema = Schema(domainsid, invocationid=invocationid, + schemadn=names.schemadn) + + if backend_type == "ldb": + provision_backend = LDBBackend(backend_type, paths=paths, + lp=lp, credentials=credentials, + names=names, logger=logger) + elif backend_type == "existing": + provision_backend = ExistingBackend(backend_type, paths=paths, + lp=lp, credentials=credentials, + names=names, logger=logger, + ldap_backend_forced_uri=ldap_backend_forced_uri) + elif backend_type == "fedora-ds": + provision_backend = FDSBackend(backend_type, paths=paths, + lp=lp, credentials=credentials, + names=names, logger=logger, domainsid=domainsid, + schema=schema, hostname=hostname, ldapadminpass=ldapadminpass, + slapd_path=slapd_path, + ldap_backend_extra_port=ldap_backend_extra_port, + ldap_dryrun_mode=ldap_dryrun_mode, root=root, + setup_ds_path=setup_ds_path, + ldap_backend_forced_uri=ldap_backend_forced_uri) + elif backend_type == "openldap": + provision_backend = OpenLDAPBackend(backend_type, paths=paths, + lp=lp, credentials=credentials, + names=names, logger=logger, domainsid=domainsid, + schema=schema, hostname=hostname, ldapadminpass=ldapadminpass, + slapd_path=slapd_path, + ldap_backend_extra_port=ldap_backend_extra_port, + ldap_dryrun_mode=ldap_dryrun_mode, ol_mmr_urls=ol_mmr_urls, + nosync=nosync, + ldap_backend_forced_uri=ldap_backend_forced_uri) + else: + raise ValueError("Unknown LDAP backend type selected") + + provision_backend.init() + provision_backend.start() + + # only install a new shares config db if there is none + if not os.path.exists(paths.shareconf): + logger.info("Setting up share.ldb") + share_ldb = Ldb(paths.shareconf, session_info=session_info, + lp=lp) + share_ldb.load_ldif_file_add(setup_path("share.ldif")) + + logger.info("Setting up secrets.ldb") + secrets_ldb = setup_secretsdb(paths, + session_info=session_info, + backend_credentials=provision_backend.secrets_credentials, lp=lp) + + try: + logger.info("Setting up the registry") + setup_registry(paths.hklm, session_info, + lp=lp) + + logger.info("Setting up the privileges database") + setup_privileges(paths.privilege, session_info, lp=lp) + + logger.info("Setting up idmap db") + idmap = setup_idmapdb(paths.idmapdb, + session_info=session_info, lp=lp) + + logger.info("Setting up SAM db") + samdb = setup_samdb(paths.samdb, session_info, + provision_backend, lp, names, logger=logger, + domainsid=domainsid, schema=schema, domainguid=domainguid, + policyguid=policyguid, policyguid_dc=policyguid_dc, + fill=samdb_fill, adminpass=adminpass, krbtgtpass=krbtgtpass, + invocationid=invocationid, machinepass=machinepass, + dnspass=dnspass, ntdsguid=ntdsguid, serverrole=serverrole, + dom_for_fun_level=dom_for_fun_level, am_rodc=am_rodc, + next_rid=next_rid) + + if serverrole == "domain controller": + if paths.netlogon is None: + logger.info("Existing smb.conf does not have a [netlogon] share, but you are configuring a DC.") + logger.info("Please either remove %s or see the template at %s" % + (paths.smbconf, setup_path("provision.smb.conf.dc"))) + assert paths.netlogon is not None + + if paths.sysvol is None: + logger.info("Existing smb.conf does not have a [sysvol] share, but you" + " are configuring a DC.") + logger.info("Please either remove %s or see the template at %s" % + (paths.smbconf, setup_path("provision.smb.conf.dc"))) + assert paths.sysvol is not None + + if not os.path.isdir(paths.netlogon): + os.makedirs(paths.netlogon, 0755) + + if samdb_fill == FILL_FULL: + setup_name_mappings(samdb, idmap, str(domainsid), names.domaindn, + root_uid=root_uid, nobody_uid=nobody_uid, + users_gid=users_gid, wheel_gid=wheel_gid) + + if serverrole == "domain controller": + # Set up group policies (domain policy and domain controller + # policy) + create_default_gpo(paths.sysvol, names.dnsdomain, policyguid, + policyguid_dc) + setsysvolacl(samdb, paths.netlogon, paths.sysvol, wheel_gid, + domainsid, names.dnsdomain, names.domaindn, lp) + + logger.info("Setting up sam.ldb rootDSE marking as synchronized") + setup_modify_ldif(samdb, setup_path("provision_rootdse_modify.ldif")) + + secretsdb_self_join(secrets_ldb, domain=names.domain, + realm=names.realm, dnsdomain=names.dnsdomain, + netbiosname=names.netbiosname, domainsid=domainsid, + machinepass=machinepass, secure_channel_type=SEC_CHAN_BDC) + + # Now set up the right msDS-SupportedEncryptionTypes into the DB + # In future, this might be determined from some configuration + kerberos_enctypes = str(ENC_ALL_TYPES) + + try: + msg = ldb.Message(ldb.Dn(samdb, + samdb.searchone("distinguishedName", + expression="samAccountName=%s$" % names.netbiosname, + scope=ldb.SCOPE_SUBTREE))) + msg["msDS-SupportedEncryptionTypes"] = ldb.MessageElement( + elements=kerberos_enctypes, flags=ldb.FLAG_MOD_REPLACE, + name="msDS-SupportedEncryptionTypes") + samdb.modify(msg) + except ldb.LdbError, (enum, estr): + if enum != ldb.ERR_NO_SUCH_ATTRIBUTE: + # It might be that this attribute does not exist in this schema + raise + + if serverrole == "domain controller": + secretsdb_setup_dns(secrets_ldb, names, + paths.private_dir, 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) + + # Only make a zone file on the first DC, it should be + # replicated with DNS replication + create_zone_file(lp, logger, paths, targetdir, + dnsdomain=names.dnsdomain, hostip=hostip, hostip6=hostip6, + hostname=names.hostname, realm=names.realm, + domainguid=domainguid, ntdsguid=names.ntdsguid) + + create_named_conf(paths, realm=names.realm, + dnsdomain=names.dnsdomain, private_dir=paths.private_dir) + + create_named_txt(paths.namedtxt, + realm=names.realm, dnsdomain=names.dnsdomain, + private_dir=paths.private_dir, + keytab_name=paths.dns_keytab) + logger.info("See %s for an example configuration include file for BIND", paths.namedconf) + logger.info("and %s for further documentation required for secure DNS " + "updates", paths.namedtxt) + + lastProvisionUSNs = get_last_provision_usn(samdb) + maxUSN = get_max_usn(samdb, str(names.rootdn)) + if lastProvisionUSNs is not None: + update_provision_usn(samdb, 0, maxUSN, 1) + else: + set_provision_usn(samdb, 0, maxUSN) + + create_krb5_conf(paths.krb5conf, + dnsdomain=names.dnsdomain, hostname=names.hostname, + realm=names.realm) + logger.info("A Kerberos configuration suitable for Samba 4 has been " + "generated at %s", paths.krb5conf) + + if serverrole == "domain controller": + create_dns_update_list(lp, logger, paths) + + provision_backend.post_setup() + provision_backend.shutdown() + + create_phpldapadmin_config(paths.phpldapadminconfig, + ldapi_url) + except Exception: + secrets_ldb.transaction_cancel() + raise + + # Now commit the secrets.ldb to disk + secrets_ldb.transaction_commit() + + # the commit creates the dns.keytab, now chown it + dns_keytab_path = os.path.join(paths.private_dir, paths.dns_keytab) + if os.path.isfile(dns_keytab_path) and paths.bind_gid is not None: + try: + os.chmod(dns_keytab_path, 0640) + os.chown(dns_keytab_path, -1, paths.bind_gid) + except OSError: + if not os.environ.has_key('SAMBA_SELFTEST'): + logger.info("Failed to chown %s to bind gid %u", + dns_keytab_path, paths.bind_gid) + + + logger.info("Please install the phpLDAPadmin configuration located at %s into /etc/phpldapadmin/config.php", + paths.phpldapadminconfig) + + logger.info("Once the above files are installed, your Samba4 server will be ready to use") + logger.info("Server Role: %s" % serverrole) + logger.info("Hostname: %s" % names.hostname) + logger.info("NetBIOS Domain: %s" % names.domain) + logger.info("DNS Domain: %s" % names.dnsdomain) + logger.info("DOMAIN SID: %s" % str(domainsid)) + if samdb_fill == FILL_FULL: + logger.info("Admin password: %s" % adminpass) + if provision_backend.type is not "ldb": + if provision_backend.credentials.get_bind_dn() is not None: + logger.info("LDAP Backend Admin DN: %s" % + provision_backend.credentials.get_bind_dn()) + else: + logger.info("LDAP Admin User: %s" % + provision_backend.credentials.get_username()) + + logger.info("LDAP Admin Password: %s" % + provision_backend.credentials.get_password()) + + if provision_backend.slapd_command_escaped is not None: + # now display slapd_command_file.txt to show how slapd must be + # started next time + logger.info("Use later the following commandline to start slapd, then Samba:") + logger.info(provision_backend.slapd_command_escaped) + logger.info("This slapd-Commandline is also stored under: %s/ldap_backend_startup.sh", + provision_backend.ldapdir) + + result = ProvisionResult() + result.domaindn = domaindn + result.paths = paths + result.lp = lp + result.samdb = samdb + return result + + +def provision_become_dc(smbconf=None, targetdir=None, + realm=None, rootdn=None, domaindn=None, schemadn=None, configdn=None, + serverdn=None, domain=None, hostname=None, domainsid=None, + adminpass=None, krbtgtpass=None, domainguid=None, policyguid=None, + policyguid_dc=None, invocationid=None, machinepass=None, dnspass=None, + root=None, nobody=None, users=None, wheel=None, backup=None, + serverrole=None, ldap_backend=None, ldap_backend_type=None, + sitename=None, debuglevel=1): + + logger = logging.getLogger("provision") + samba.set_debug_level(debuglevel) + + res = provision(logger, system_session(), None, + smbconf=smbconf, targetdir=targetdir, samdb_fill=FILL_DRS, + realm=realm, rootdn=rootdn, domaindn=domaindn, schemadn=schemadn, + configdn=configdn, serverdn=serverdn, domain=domain, + hostname=hostname, hostip="127.0.0.1", domainsid=domainsid, + machinepass=machinepass, serverrole="domain controller", + sitename=sitename) + res.lp.set("debuglevel", str(debuglevel)) + return res + + +def create_phpldapadmin_config(path, ldapi_uri): + """Create a PHP LDAP admin configuration file. + + :param path: Path to write the configuration to. + """ + setup_file(setup_path("phpldapadmin-config.php"), path, + {"S4_LDAPI_URI": ldapi_uri}) + + +def create_zone_file(lp, logger, paths, targetdir, dnsdomain, + hostip, hostip6, hostname, realm, domainguid, + ntdsguid): + """Write out a DNS zone file, from the info in the current database. + + :param paths: paths object + :param dnsdomain: DNS Domain name + :param domaindn: DN of the Domain + :param hostip: Local IPv4 IP + :param hostip6: Local IPv6 IP + :param hostname: Local hostname + :param realm: Realm name + :param domainguid: GUID of the domain. + :param ntdsguid: GUID of the hosts nTDSDSA record. + """ + assert isinstance(domainguid, str) + + if hostip6 is not None: + hostip6_base_line = " IN AAAA " + hostip6 + hostip6_host_line = hostname + " IN AAAA " + hostip6 + gc_msdcs_ip6_line = "gc._msdcs IN AAAA " + hostip6 + else: + hostip6_base_line = "" + hostip6_host_line = "" + gc_msdcs_ip6_line = "" + + if hostip is not None: + hostip_base_line = " IN A " + hostip + hostip_host_line = hostname + " IN A " + hostip + gc_msdcs_ip_line = "gc._msdcs IN A " + hostip + else: + hostip_base_line = "" + hostip_host_line = "" + gc_msdcs_ip_line = "" + + dns_dir = os.path.dirname(paths.dns) + + try: + shutil.rmtree(dns_dir, True) + except OSError: + pass + + os.mkdir(dns_dir, 0775) + + # we need to freeze the zone while we update the contents + if targetdir is None: + rndc = ' '.join(lp.get("rndc command")) + os.system(rndc + " freeze " + lp.get("realm")) + + setup_file(setup_path("provision.zone"), paths.dns, { + "HOSTNAME": hostname, + "DNSDOMAIN": dnsdomain, + "REALM": realm, + "HOSTIP_BASE_LINE": hostip_base_line, + "HOSTIP_HOST_LINE": hostip_host_line, + "DOMAINGUID": domainguid, + "DATESTRING": time.strftime("%Y%m%d%H"), + "DEFAULTSITE": DEFAULTSITE, + "NTDSGUID": ntdsguid, + "HOSTIP6_BASE_LINE": hostip6_base_line, + "HOSTIP6_HOST_LINE": hostip6_host_line, + "GC_MSDCS_IP_LINE": gc_msdcs_ip_line, + "GC_MSDCS_IP6_LINE": gc_msdcs_ip6_line, + }) + + # note that we use no variable substitution on this file + # the substitution is done at runtime by samba_dnsupdate + setup_file(setup_path("dns_update_list"), paths.dns_update_list, None) + + # and the SPN update list + setup_file(setup_path("spn_update_list"), paths.spn_update_list, None) + + if paths.bind_gid is not None: + try: + os.chown(dns_dir, -1, paths.bind_gid) + os.chown(paths.dns, -1, paths.bind_gid) + # chmod needed to cope with umask + os.chmod(dns_dir, 0775) + os.chmod(paths.dns, 0664) + except OSError: + if not os.environ.has_key('SAMBA_SELFTEST'): + logger.error("Failed to chown %s to bind gid %u" % ( + dns_dir, paths.bind_gid)) + + if targetdir is None: + os.system(rndc + " unfreeze " + lp.get("realm")) + + +def create_dns_update_list(lp, logger, paths): + """Write out a dns_update_list file""" + # note that we use no variable substitution on this file + # the substitution is done at runtime by samba_dnsupdate + setup_file(setup_path("dns_update_list"), paths.dns_update_list, None) + setup_file(setup_path("spn_update_list"), paths.spn_update_list, None) + + +def create_named_conf(paths, realm, dnsdomain, + private_dir): + """Write out a file containing zone statements suitable for inclusion in a + named.conf file (including GSS-TSIG configuration). + + :param paths: all paths + :param realm: Realm name + :param dnsdomain: DNS Domain name + :param private_dir: Path to private directory + :param keytab_name: File name of DNS keytab file + """ + + setup_file(setup_path("named.conf"), paths.namedconf, { + "DNSDOMAIN": dnsdomain, + "REALM": realm, + "ZONE_FILE": paths.dns, + "REALM_WC": "*." + ".".join(realm.split(".")[1:]), + "NAMED_CONF": paths.namedconf, + "NAMED_CONF_UPDATE": paths.namedconf_update + }) + + setup_file(setup_path("named.conf.update"), paths.namedconf_update) + + +def create_named_txt(path, realm, dnsdomain, private_dir, + keytab_name): + """Write out a file containing zone statements suitable for inclusion in a + named.conf file (including GSS-TSIG configuration). + + :param path: Path of the new named.conf file. + :param realm: Realm name + :param dnsdomain: DNS Domain name + :param private_dir: Path to private directory + :param keytab_name: File name of DNS keytab file + """ + setup_file(setup_path("named.txt"), path, { + "DNSDOMAIN": dnsdomain, + "REALM": realm, + "DNS_KEYTAB": keytab_name, + "DNS_KEYTAB_ABS": os.path.join(private_dir, keytab_name), + "PRIVATE_DIR": private_dir + }) + + +def create_krb5_conf(path, dnsdomain, hostname, realm): + """Write out a file containing zone statements suitable for inclusion in a + named.conf file (including GSS-TSIG configuration). + + :param path: Path of the new named.conf file. + :param dnsdomain: DNS Domain name + :param hostname: Local hostname + :param realm: Realm name + """ + setup_file(setup_path("krb5.conf"), path, { + "DNSDOMAIN": dnsdomain, + "HOSTNAME": hostname, + "REALM": realm, + }) + + +class ProvisioningError(Exception): + """A generic provision error.""" + + def __init__(self, value): + self.value = value + + def __str__(self): + return "ProvisioningError: " + self.value + + +class InvalidNetbiosName(Exception): + """A specified name was not a valid NetBIOS name.""" + def __init__(self, name): + super(InvalidNetbiosName, self).__init__( + "The name '%r' is not a valid NetBIOS name" % name) diff --git a/source4/scripting/python/samba/provision/backend.py b/source4/scripting/python/samba/provision/backend.py new file mode 100644 index 0000000000..f9dbba85f6 --- /dev/null +++ b/source4/scripting/python/samba/provision/backend.py @@ -0,0 +1,772 @@ +# +# Unix SMB/CIFS implementation. +# backend code for provisioning a Samba4 server + +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2008 +# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2008-2009 +# Copyright (C) Oliver Liebel <oliver@itc.li> 2008-2009 +# +# Based on the original in EJS: +# Copyright (C) Andrew Tridgell <tridge@samba.org> 2005 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +"""Functions for setting up a Samba configuration (LDB and LDAP backends).""" + +from base64 import b64encode +import errno +import ldb +import os +import sys +import uuid +import time +import shutil +import subprocess +import urllib + +from ldb import SCOPE_BASE, SCOPE_ONELEVEL, LdbError, timestring + +from samba import Ldb, read_and_sub_file, setup_file +from samba.credentials import Credentials, DONT_USE_KERBEROS +from samba.schema import Schema + +class SlapdAlreadyRunning(Exception): + + def __init__(self, uri): + self.ldapi_uri = uri + super(SlapdAlreadyRunning, self).__init__("Another slapd Instance " + "seems already running on this host, listening to %s." % + self.ldapi_uri) + + +class ProvisionBackend(object): + def __init__(self, backend_type, paths=None, lp=None, + credentials=None, names=None, logger=None): + """Provision a backend for samba4""" + self.paths = paths + self.lp = lp + self.credentials = credentials + self.names = names + self.logger = logger + + self.type = backend_type + + # Set a default - the code for "existing" below replaces this + self.ldap_backend_type = backend_type + + def init(self): + """Initialize the backend.""" + raise NotImplementedError(self.init) + + def start(self): + """Start the backend.""" + raise NotImplementedError(self.start) + + def shutdown(self): + """Shutdown the backend.""" + raise NotImplementedError(self.shutdown) + + def post_setup(self): + """Post setup.""" + raise NotImplementedError(self.post_setup) + + +class LDBBackend(ProvisionBackend): + + def init(self): + self.credentials = None + self.secrets_credentials = None + + # Wipe the old sam.ldb databases away + shutil.rmtree(self.paths.samdb + ".d", True) + + def start(self): + pass + + def shutdown(self): + pass + + def post_setup(self): + pass + + +class ExistingBackend(ProvisionBackend): + + def __init__(self, backend_type, paths=None, lp=None, + credentials=None, names=None, logger=None, ldapi_uri=None): + + super(ExistingBackend, self).__init__(backend_type=backend_type, + paths=paths, lp=lp, + credentials=credentials, names=names, logger=logger, + ldap_backend_forced_uri=ldapi_uri) + + def init(self): + # Check to see that this 'existing' LDAP backend in fact exists + ldapi_db = Ldb(self.ldapi_uri, credentials=self.credentials) + ldapi_db.search(base="", scope=SCOPE_BASE, + expression="(objectClass=OpenLDAProotDSE)") + + # If we have got here, then we must have a valid connection to the LDAP + # server, with valid credentials supplied This caused them to be set + # into the long-term database later in the script. + self.secrets_credentials = self.credentials + + # For now, assume existing backends at least emulate OpenLDAP + self.ldap_backend_type = "openldap" + + +class LDAPBackend(ProvisionBackend): + + def __init__(self, backend_type, paths=None, lp=None, + credentials=None, names=None, logger=None, domainsid=None, + schema=None, hostname=None, ldapadminpass=None, + slapd_path=None, ldap_backend_extra_port=None, + ldap_backend_forced_uri=None, ldap_dryrun_mode=False): + + super(LDAPBackend, self).__init__(backend_type=backend_type, + paths=paths, lp=lp, + credentials=credentials, names=names, logger=logger) + + self.domainsid = domainsid + self.schema = schema + self.hostname = hostname + + self.ldapdir = os.path.join(paths.private_dir, "ldap") + self.ldapadminpass = ldapadminpass + + self.slapd_path = slapd_path + self.slapd_command = None + self.slapd_command_escaped = None + self.slapd_pid = os.path.join(self.ldapdir, "slapd.pid") + + self.ldap_backend_extra_port = ldap_backend_extra_port + self.ldap_dryrun_mode = ldap_dryrun_mode + + if ldap_backend_forced_uri is not None: + self.ldap_uri = ldap_backend_forced_uri + else: + self.ldap_uri = "ldapi://%s" % urllib.quote( + os.path.join(self.ldapdir, "ldapi"), safe="") + + if not os.path.exists(self.ldapdir): + os.mkdir(self.ldapdir) + + def init(self): + from samba.provision import ProvisioningError + # we will shortly start slapd with ldapi for final provisioning. first + # check with ldapsearch -> rootDSE via self.ldap_uri if another + # instance of slapd is already running + try: + ldapi_db = Ldb(self.ldap_uri) + ldapi_db.search(base="", scope=SCOPE_BASE, + expression="(objectClass=OpenLDAProotDSE)") + try: + f = open(self.slapd_pid, "r") + except IOError, err: + if err != errno.ENOENT: + raise + else: + p = f.read() + f.close() + self.logger.info("Check for slapd Process with PID: %s and terminate it manually." % p) + raise SlapdAlreadyRunning(self.ldap_uri) + except LdbError: + # XXX: We should never be catching all Ldb errors + pass + + # Try to print helpful messages when the user has not specified the + # path to slapd + if self.slapd_path is None: + raise ProvisioningError("Warning: LDAP-Backend must be setup with path to slapd, e.g. --slapd-path=\"/usr/local/libexec/slapd\"!") + if not os.path.exists(self.slapd_path): + self.logger.warning("Path (%s) to slapd does not exist!", + self.slapd_path) + + if not os.path.isdir(self.ldapdir): + os.makedirs(self.ldapdir, 0700) + + # Put the LDIF of the schema into a database so we can search on + # it to generate schema-dependent configurations in Fedora DS and + # OpenLDAP + schemadb_path = os.path.join(self.ldapdir, "schema-tmp.ldb") + try: + os.unlink(schemadb_path) + except OSError: + pass + + self.schema.write_to_tmp_ldb(schemadb_path) + + self.credentials = Credentials() + self.credentials.guess(self.lp) + # Kerberos to an ldapi:// backend makes no sense + self.credentials.set_kerberos_state(DONT_USE_KERBEROS) + self.credentials.set_password(self.ldapadminpass) + + self.secrets_credentials = Credentials() + self.secrets_credentials.guess(self.lp) + # Kerberos to an ldapi:// backend makes no sense + self.secrets_credentials.set_kerberos_state(DONT_USE_KERBEROS) + self.secrets_credentials.set_username("samba-admin") + self.secrets_credentials.set_password(self.ldapadminpass) + + self.provision() + + def provision(self): + pass + + def start(self): + from samba.provision import ProvisioningError + self.slapd_command_escaped = "\'" + "\' \'".join(self.slapd_command) + "\'" + f = open(os.path.join(self.ldapdir, "ldap_backend_startup.sh"), 'w') + try: + f.write("#!/bin/sh\n" + self.slapd_command_escaped + "\n") + finally: + f.close() + + # Now start the slapd, so we can provision onto it. We keep the + # subprocess context around, to kill this off at the successful + # end of the script + self.slapd = subprocess.Popen(self.slapd_provision_command, + close_fds=True, shell=False) + + count = 0 + while self.slapd.poll() is None: + # Wait until the socket appears + try: + ldapi_db = Ldb(self.ldap_uri, lp=self.lp, credentials=self.credentials) + ldapi_db.search(base="", scope=SCOPE_BASE, + expression="(objectClass=OpenLDAProotDSE)") + # If we have got here, then we must have a valid connection to + # the LDAP server! + return + except LdbError: + time.sleep(1) + count = count + 1 + + if count > 15: + self.logger.error("Could not connect to slapd started with: %s" % "\'" + "\' \'".join(self.slapd_provision_command) + "\'") + raise ProvisioningError("slapd never accepted a connection within 15 seconds of starting") + + self.logger.error("Could not start slapd with: %s" % "\'" + "\' \'".join(self.slapd_provision_command) + "\'") + raise ProvisioningError("slapd died before we could make a connection to it") + + def shutdown(self): + # if an LDAP backend is in use, terminate slapd after final provision + # and check its proper termination + if self.slapd.poll() is None: + # Kill the slapd + if getattr(self.slapd, "terminate", None) is not None: + self.slapd.terminate() + else: + # Older python versions don't have .terminate() + import signal + os.kill(self.slapd.pid, signal.SIGTERM) + + # and now wait for it to die + self.slapd.communicate() + + def post_setup(self): + pass + + +class OpenLDAPBackend(LDAPBackend): + + def __init__(self, backend_type, paths=None, lp=None, + credentials=None, names=None, logger=None, domainsid=None, + schema=None, hostname=None, ldapadminpass=None, slapd_path=None, + ldap_backend_extra_port=None, ldap_dryrun_mode=False, + ol_mmr_urls=None, nosync=False, ldap_backend_forced_uri=None): + from samba.provision import setup_path + super(OpenLDAPBackend, self).__init__( backend_type=backend_type, + paths=paths, lp=lp, + credentials=credentials, names=names, logger=logger, + domainsid=domainsid, schema=schema, hostname=hostname, + ldapadminpass=ldapadminpass, slapd_path=slapd_path, + ldap_backend_extra_port=ldap_backend_extra_port, + ldap_backend_forced_uri=ldap_backend_forced_uri, + ldap_dryrun_mode=ldap_dryrun_mode) + + self.ol_mmr_urls = ol_mmr_urls + self.nosync = nosync + + self.slapdconf = os.path.join(self.ldapdir, "slapd.conf") + self.modulesconf = os.path.join(self.ldapdir, "modules.conf") + self.memberofconf = os.path.join(self.ldapdir, "memberof.conf") + self.olmmrserveridsconf = os.path.join(self.ldapdir, "mmr_serverids.conf") + self.olmmrsyncreplconf = os.path.join(self.ldapdir, "mmr_syncrepl.conf") + self.olcdir = os.path.join(self.ldapdir, "slapd.d") + self.olcseedldif = os.path.join(self.ldapdir, "olc_seed.ldif") + + self.schema = Schema(self.domainsid, + schemadn=self.names.schemadn, files=[ + setup_path("schema_samba4.ldif")]) + + def setup_db_config(self, dbdir): + """Setup a Berkeley database. + + :param dbdir: Database directory. + """ + from samba.provision import setup_path + if not os.path.isdir(os.path.join(dbdir, "bdb-logs")): + os.makedirs(os.path.join(dbdir, "bdb-logs"), 0700) + if not os.path.isdir(os.path.join(dbdir, "tmp")): + os.makedirs(os.path.join(dbdir, "tmp"), 0700) + + setup_file(setup_path("DB_CONFIG"), + os.path.join(dbdir, "DB_CONFIG"), {"LDAPDBDIR": dbdir}) + + def provision(self): + from samba.provision import ProvisioningError, setup_path + # Wipe the directories so we can start + shutil.rmtree(os.path.join(self.ldapdir, "db"), True) + + # Allow the test scripts to turn off fsync() for OpenLDAP as for TDB + # and LDB + nosync_config = "" + if self.nosync: + nosync_config = "dbnosync" + + lnkattr = self.schema.linked_attributes() + refint_attributes = "" + memberof_config = "# Generated from Samba4 schema\n" + for att in lnkattr.keys(): + if lnkattr[att] is not None: + refint_attributes = refint_attributes + " " + att + + memberof_config += read_and_sub_file( + setup_path("memberof.conf"), { + "MEMBER_ATTR": att, + "MEMBEROF_ATTR" : lnkattr[att] }) + + refint_config = read_and_sub_file(setup_path("refint.conf"), + { "LINK_ATTRS" : refint_attributes}) + + attrs = ["linkID", "lDAPDisplayName"] + res = self.schema.ldb.search(expression="(&(objectclass=attributeSchema)(searchFlags:1.2.840.113556.1.4.803:=1))", base=self.names.schemadn, scope=SCOPE_ONELEVEL, attrs=attrs) + index_config = "" + for i in range (0, len(res)): + index_attr = res[i]["lDAPDisplayName"][0] + if index_attr == "objectGUID": + index_attr = "entryUUID" + + index_config += "index " + index_attr + " eq\n" + + # generate serverids, ldap-urls and syncrepl-blocks for mmr hosts + mmr_on_config = "" + mmr_replicator_acl = "" + mmr_serverids_config = "" + mmr_syncrepl_schema_config = "" + mmr_syncrepl_config_config = "" + mmr_syncrepl_user_config = "" + + if self.ol_mmr_urls is not None: + # For now, make these equal + mmr_pass = self.ldapadminpass + + url_list = filter(None,self.ol_mmr_urls.split(',')) + for url in url_list: + self.logger.info("Using LDAP-URL: "+url) + if len(url_list) == 1: + raise ProvisioningError("At least 2 LDAP-URLs needed for MMR!") + + mmr_on_config = "MirrorMode On" + mmr_replicator_acl = " by dn=cn=replicator,cn=samba read" + serverid = 0 + for url in url_list: + serverid = serverid + 1 + mmr_serverids_config += read_and_sub_file( + setup_path("mmr_serverids.conf"), { + "SERVERID": str(serverid), + "LDAPSERVER": url }) + rid = serverid * 10 + rid = rid + 1 + mmr_syncrepl_schema_config += read_and_sub_file( + setup_path("mmr_syncrepl.conf"), { + "RID" : str(rid), + "MMRDN": self.names.schemadn, + "LDAPSERVER" : url, + "MMR_PASSWORD": mmr_pass}) + + rid = rid + 1 + mmr_syncrepl_config_config += read_and_sub_file( + setup_path("mmr_syncrepl.conf"), { + "RID" : str(rid), + "MMRDN": self.names.configdn, + "LDAPSERVER" : url, + "MMR_PASSWORD": mmr_pass}) + + rid = rid + 1 + mmr_syncrepl_user_config += read_and_sub_file( + setup_path("mmr_syncrepl.conf"), { + "RID" : str(rid), + "MMRDN": self.names.domaindn, + "LDAPSERVER" : url, + "MMR_PASSWORD": mmr_pass }) + # OpenLDAP cn=config initialisation + olc_syncrepl_config = "" + olc_mmr_config = "" + # if mmr = yes, generate cn=config-replication directives + # and olc_seed.lif for the other mmr-servers + if self.ol_mmr_urls is not None: + serverid = 0 + olc_serverids_config = "" + olc_syncrepl_seed_config = "" + olc_mmr_config += read_and_sub_file( + setup_path("olc_mmr.conf"), {}) + rid = 500 + for url in url_list: + serverid = serverid + 1 + olc_serverids_config += read_and_sub_file( + setup_path("olc_serverid.conf"), { + "SERVERID" : str(serverid), "LDAPSERVER" : url }) + + rid = rid + 1 + olc_syncrepl_config += read_and_sub_file( + setup_path("olc_syncrepl.conf"), { + "RID" : str(rid), "LDAPSERVER" : url, + "MMR_PASSWORD": mmr_pass}) + + olc_syncrepl_seed_config += read_and_sub_file( + setup_path("olc_syncrepl_seed.conf"), { + "RID" : str(rid), "LDAPSERVER" : url}) + + setup_file(setup_path("olc_seed.ldif"), self.olcseedldif, + {"OLC_SERVER_ID_CONF": olc_serverids_config, + "OLC_PW": self.ldapadminpass, + "OLC_SYNCREPL_CONF": olc_syncrepl_seed_config}) + # end olc + + setup_file(setup_path("slapd.conf"), self.slapdconf, + {"DNSDOMAIN": self.names.dnsdomain, + "LDAPDIR": self.ldapdir, + "DOMAINDN": self.names.domaindn, + "CONFIGDN": self.names.configdn, + "SCHEMADN": self.names.schemadn, + "MEMBEROF_CONFIG": memberof_config, + "MIRRORMODE": mmr_on_config, + "REPLICATOR_ACL": mmr_replicator_acl, + "MMR_SERVERIDS_CONFIG": mmr_serverids_config, + "MMR_SYNCREPL_SCHEMA_CONFIG": mmr_syncrepl_schema_config, + "MMR_SYNCREPL_CONFIG_CONFIG": mmr_syncrepl_config_config, + "MMR_SYNCREPL_USER_CONFIG": mmr_syncrepl_user_config, + "OLC_SYNCREPL_CONFIG": olc_syncrepl_config, + "OLC_MMR_CONFIG": olc_mmr_config, + "REFINT_CONFIG": refint_config, + "INDEX_CONFIG": index_config, + "NOSYNC": nosync_config}) + + self.setup_db_config(os.path.join(self.ldapdir, "db", "user")) + self.setup_db_config(os.path.join(self.ldapdir, "db", "config")) + self.setup_db_config(os.path.join(self.ldapdir, "db", "schema")) + + if not os.path.exists(os.path.join(self.ldapdir, "db", "samba", "cn=samba")): + os.makedirs(os.path.join(self.ldapdir, "db", "samba", "cn=samba"), 0700) + + setup_file(setup_path("cn=samba.ldif"), + os.path.join(self.ldapdir, "db", "samba", "cn=samba.ldif"), + { "UUID": str(uuid.uuid4()), + "LDAPTIME": timestring(int(time.time()))} ) + setup_file(setup_path("cn=samba-admin.ldif"), + os.path.join(self.ldapdir, "db", "samba", "cn=samba", "cn=samba-admin.ldif"), + {"LDAPADMINPASS_B64": b64encode(self.ldapadminpass), + "UUID": str(uuid.uuid4()), + "LDAPTIME": timestring(int(time.time()))} ) + + if self.ol_mmr_urls is not None: + setup_file(setup_path("cn=replicator.ldif"), + os.path.join(self.ldapdir, "db", "samba", "cn=samba", "cn=replicator.ldif"), + {"MMR_PASSWORD_B64": b64encode(mmr_pass), + "UUID": str(uuid.uuid4()), + "LDAPTIME": timestring(int(time.time()))} ) + + mapping = "schema-map-openldap-2.3" + backend_schema = "backend-schema.schema" + + f = open(setup_path(mapping), 'r') + backend_schema_data = self.schema.convert_to_openldap( + "openldap", f.read()) + assert backend_schema_data is not None + f = open(os.path.join(self.ldapdir, backend_schema), 'w') + try: + f.write(backend_schema_data) + finally: + f.close() + + # now we generate the needed strings to start slapd automatically, + if self.ldap_backend_extra_port is not None: + # When we use MMR, we can't use 0.0.0.0 as it uses the name + # specified there as part of it's clue as to it's own name, + # and not to replicate to itself + if self.ol_mmr_urls is None: + server_port_string = "ldap://0.0.0.0:%d" % self.ldap_backend_extra_port + else: + server_port_string = "ldap://%s.%s:%d" (self.names.hostname, + self.names.dnsdomain, self.ldap_backend_extra_port) + else: + server_port_string = "" + + # Prepare the 'result' information - the commands to return in + # particular + self.slapd_provision_command = [self.slapd_path, "-F" + self.olcdir, + "-h"] + + # copy this command so we have two version, one with -d0 and only + # ldapi (or the forced ldap_uri), and one with all the listen commands + self.slapd_command = list(self.slapd_provision_command) + + self.slapd_provision_command.extend([self.ldap_uri, "-d0"]) + + uris = self.ldap_uri + if server_port_string is not "": + uris = uris + " " + server_port_string + + self.slapd_command.append(uris) + + # Set the username - done here because Fedora DS still uses the admin + # DN and simple bind + self.credentials.set_username("samba-admin") + + # Wipe the old sam.ldb databases away + shutil.rmtree(self.olcdir, True) + os.makedirs(self.olcdir, 0770) + + # If we were just looking for crashes up to this point, it's a + # good time to exit before we realise we don't have OpenLDAP on + # this system + if self.ldap_dryrun_mode: + sys.exit(0) + + slapd_cmd = [self.slapd_path, "-Ttest", "-n", "0", "-f", + self.slapdconf, "-F", self.olcdir] + retcode = subprocess.call(slapd_cmd, close_fds=True, shell=False) + + if retcode != 0: + self.logger.error("conversion from slapd.conf to cn=config failed slapd started with: %s" % "\'" + "\' \'".join(slapd_cmd) + "\'") + raise ProvisioningError("conversion from slapd.conf to cn=config failed") + + if not os.path.exists(os.path.join(self.olcdir, "cn=config.ldif")): + raise ProvisioningError("conversion from slapd.conf to cn=config failed") + + # Don't confuse the admin by leaving the slapd.conf around + os.remove(self.slapdconf) + + +class FDSBackend(LDAPBackend): + + def __init__(self, backend_type, paths=None, lp=None, + credentials=None, names=None, logger=None, domainsid=None, + schema=None, hostname=None, ldapadminpass=None, slapd_path=None, + ldap_backend_extra_port=None, ldap_dryrun_mode=False, root=None, + setup_ds_path=None): + + from samba.provision import setup_path + + super(FDSBackend, self).__init__(backend_type=backend_type, + paths=paths, lp=lp, + credentials=credentials, names=names, logger=logger, + domainsid=domainsid, schema=schema, hostname=hostname, + ldapadminpass=ldapadminpass, slapd_path=slapd_path, + ldap_backend_extra_port=ldap_backend_extra_port, + ldap_backend_forced_uri=ldap_backend_forced_uri, + ldap_dryrun_mode=ldap_dryrun_mode) + + self.root = root + self.setup_ds_path = setup_ds_path + self.ldap_instance = self.names.netbiosname.lower() + + self.sambadn = "CN=Samba" + + self.fedoradsinf = os.path.join(self.ldapdir, "fedorads.inf") + self.partitions_ldif = os.path.join(self.ldapdir, + "fedorads-partitions.ldif") + self.sasl_ldif = os.path.join(self.ldapdir, "fedorads-sasl.ldif") + self.dna_ldif = os.path.join(self.ldapdir, "fedorads-dna.ldif") + self.pam_ldif = os.path.join(self.ldapdir, "fedorads-pam.ldif") + self.refint_ldif = os.path.join(self.ldapdir, "fedorads-refint.ldif") + self.linked_attrs_ldif = os.path.join(self.ldapdir, + "fedorads-linked-attributes.ldif") + self.index_ldif = os.path.join(self.ldapdir, "fedorads-index.ldif") + self.samba_ldif = os.path.join(self.ldapdir, "fedorads-samba.ldif") + + self.samba3_schema = setup_path( + "../../examples/LDAP/samba.schema") + self.samba3_ldif = os.path.join(self.ldapdir, "samba3.ldif") + + self.retcode = subprocess.call(["bin/oLschema2ldif", + "-I", self.samba3_schema, + "-O", self.samba3_ldif, + "-b", self.names.domaindn], + close_fds=True, shell=False) + + if self.retcode != 0: + raise Exception("Unable to convert Samba 3 schema.") + + self.schema = Schema( + self.domainsid, + schemadn=self.names.schemadn, + files=[setup_path("schema_samba4.ldif"), self.samba3_ldif], + additional_prefixmap=["1000:1.3.6.1.4.1.7165.2.1", + "1001:1.3.6.1.4.1.7165.2.2"]) + + def provision(self): + from samba.provision import ProvisioningError, setup_path + if self.ldap_backend_extra_port is not None: + serverport = "ServerPort=%d" % self.ldap_backend_extra_port + else: + serverport = "" + + setup_file(setup_path("fedorads.inf"), self.fedoradsinf, + {"ROOT": self.root, + "HOSTNAME": self.hostname, + "DNSDOMAIN": self.names.dnsdomain, + "LDAPDIR": self.ldapdir, + "DOMAINDN": self.names.domaindn, + "LDAP_INSTANCE": self.ldap_instance, + "LDAPMANAGERDN": self.names.ldapmanagerdn, + "LDAPMANAGERPASS": self.ldapadminpass, + "SERVERPORT": serverport}) + + setup_file(setup_path("fedorads-partitions.ldif"), + self.partitions_ldif, + {"CONFIGDN": self.names.configdn, + "SCHEMADN": self.names.schemadn, + "SAMBADN": self.sambadn, + }) + + setup_file(setup_path("fedorads-sasl.ldif"), self.sasl_ldif, + {"SAMBADN": self.sambadn, + }) + + setup_file(setup_path("fedorads-dna.ldif"), self.dna_ldif, + {"DOMAINDN": self.names.domaindn, + "SAMBADN": self.sambadn, + "DOMAINSID": str(self.domainsid), + }) + + setup_file(setup_path("fedorads-pam.ldif"), self.pam_ldif) + + lnkattr = self.schema.linked_attributes() + + refint_config = open(setup_path("fedorads-refint-delete.ldif"), 'r').read() + memberof_config = "" + index_config = "" + argnum = 3 + + for attr in lnkattr.keys(): + if lnkattr[attr] is not None: + refint_config += read_and_sub_file( + setup_path("fedorads-refint-add.ldif"), + { "ARG_NUMBER" : str(argnum), + "LINK_ATTR" : attr }) + memberof_config += read_and_sub_file( + setup_path("fedorads-linked-attributes.ldif"), + { "MEMBER_ATTR" : attr, + "MEMBEROF_ATTR" : lnkattr[attr] }) + index_config += read_and_sub_file( + setup_path("fedorads-index.ldif"), { "ATTR" : attr }) + argnum += 1 + + open(self.refint_ldif, 'w').write(refint_config) + open(self.linked_attrs_ldif, 'w').write(memberof_config) + + attrs = ["lDAPDisplayName"] + res = self.schema.ldb.search(expression="(&(objectclass=attributeSchema)(searchFlags:1.2.840.113556.1.4.803:=1))", base=self.names.schemadn, scope=SCOPE_ONELEVEL, attrs=attrs) + + for i in range (0, len(res)): + attr = res[i]["lDAPDisplayName"][0] + + if attr == "objectGUID": + attr = "nsUniqueId" + + index_config += read_and_sub_file( + setup_path("fedorads-index.ldif"), { "ATTR" : attr }) + + open(self.index_ldif, 'w').write(index_config) + + setup_file(setup_path("fedorads-samba.ldif"), self.samba_ldif, { + "SAMBADN": self.sambadn, + "LDAPADMINPASS": self.ldapadminpass + }) + + mapping = "schema-map-fedora-ds-1.0" + backend_schema = "99_ad.ldif" + + # Build a schema file in Fedora DS format + backend_schema_data = self.schema.convert_to_openldap("fedora-ds", + open(setup_path(mapping), 'r').read()) + assert backend_schema_data is not None + f = open(os.path.join(self.ldapdir, backend_schema), 'w') + try: + f.write(backend_schema_data) + finally: + f.close() + + self.credentials.set_bind_dn(self.names.ldapmanagerdn) + + # Destory the target directory, or else setup-ds.pl will complain + fedora_ds_dir = os.path.join(self.ldapdir, + "slapd-" + self.ldap_instance) + shutil.rmtree(fedora_ds_dir, True) + + self.slapd_provision_command = [self.slapd_path, "-D", fedora_ds_dir, + "-i", self.slapd_pid] + # In the 'provision' command line, stay in the foreground so we can + # easily kill it + self.slapd_provision_command.append("-d0") + + #the command for the final run is the normal script + self.slapd_command = [os.path.join(self.ldapdir, + "slapd-" + self.ldap_instance, "start-slapd")] + + # If we were just looking for crashes up to this point, it's a + # good time to exit before we realise we don't have Fedora DS on + if self.ldap_dryrun_mode: + sys.exit(0) + + # Try to print helpful messages when the user has not specified the + # path to the setup-ds tool + if self.setup_ds_path is None: + raise ProvisioningError("Fedora DS LDAP-Backend must be setup with path to setup-ds, e.g. --setup-ds-path=\"/usr/sbin/setup-ds.pl\"!") + if not os.path.exists(self.setup_ds_path): + self.logger.warning("Path (%s) to slapd does not exist!", + self.setup_ds_path) + + # Run the Fedora DS setup utility + retcode = subprocess.call([self.setup_ds_path, "--silent", "--file", + self.fedoradsinf], close_fds=True, shell=False) + if retcode != 0: + raise ProvisioningError("setup-ds failed") + + # Load samba-admin + retcode = subprocess.call([ + os.path.join(self.ldapdir, "slapd-" + self.ldap_instance, "ldif2db"), "-s", self.sambadn, "-i", self.samba_ldif], + close_fds=True, shell=False) + if retcode != 0: + raise ProvisioningError("ldif2db failed") + + def post_setup(self): + ldapi_db = Ldb(self.ldap_uri, credentials=self.credentials) + + # configure in-directory access control on Fedora DS via the aci + # attribute (over a direct ldapi:// socket) + aci = """(targetattr = "*") (version 3.0;acl "full access to all by samba-admin";allow (all)(userdn = "ldap:///CN=samba-admin,%s");)""" % self.sambadn + + m = ldb.Message() + m["aci"] = ldb.MessageElement([aci], ldb.FLAG_MOD_REPLACE, "aci") + + for dnstring in (self.names.domaindn, self.names.configdn, + self.names.schemadn): + m.dn = ldb.Dn(ldapi_db, dnstring) + ldapi_db.modify(m) diff --git a/source4/scripting/python/samba/samba3.py b/source4/scripting/python/samba/samba3.py index 179efa2700..2c323bd0b4 100644 --- a/source4/scripting/python/samba/samba3.py +++ b/source4/scripting/python/samba/samba3.py @@ -1,18 +1,16 @@ -#!/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/>. # @@ -67,7 +65,7 @@ class TdbDatabase(object): class Registry(TdbDatabase): """Simple read-only support for reading the Samba3 registry. - + :note: This object uses the same syntax for registry key paths as Samba 3. This particular format uses forward slashes for key path separators and abbreviations for the predefined key names. @@ -683,7 +681,7 @@ class ParamFile(object): section = None for i, l in enumerate(open(filename, 'r').xreadlines()): l = l.strip() - if not l: + if not l or l[0] == '#' or l[0] == ';': continue if l[0] == "[" and l[-1] == "]": section = self._sanitize_name(l[1:-1]) @@ -769,7 +767,7 @@ class Samba3(object): def get_policy_db(self): return PolicyDatabase(self.libdir_path("account_policy.tdb")) - + def get_registry(self): return Registry(self.libdir_path("registry.tdb")) diff --git a/source4/scripting/python/samba/samdb.py b/source4/scripting/python/samba/samdb.py index 39cf1d6c40..99f141e664 100644 --- a/source4/scripting/python/samba/samdb.py +++ b/source4/scripting/python/samba/samdb.py @@ -1,22 +1,22 @@ -#!/usr/bin/python +#!/usr/bin/env python # Unix SMB/CIFS implementation. -# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2008 +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2010 # Copyright (C) Matthias Dieter Wallnoefer 2009 # # Based on the original in EJS: # Copyright (C) Andrew Tridgell <tridge@samba.org> 2005 -# +# # 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/>. # @@ -24,62 +24,71 @@ """Convenience functions for using the SAM.""" import samba -import glue import ldb -from samba.idmap import IDmapDB -import pwd import time import base64 +from samba import dsdb +from samba.ndr import ndr_unpack, ndr_pack +from samba.dcerpc import drsblobs, misc __docformat__ = "restructuredText" + class SamDB(samba.Ldb): """The SAM 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) - """ + hash_oid_name = {} + def __init__(self, url=None, lp=None, modules_dir=None, session_info=None, + credentials=None, flags=0, options=None, global_schema=True, + auto_connect=True, am_rodc=None): self.lp = lp - if url is None: - url = lp.get("sam database") + if not auto_connect: + url = None + elif url is None and lp is not 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) + session_info=session_info, credentials=credentials, flags=flags, + options=options) + + if global_schema: + dsdb._dsdb_set_global_schema(self) - glue.dsdb_set_global_schema(self) + if am_rodc is not None: + dsdb._dsdb_set_am_rodc(self, am_rodc) def connect(self, url=None, flags=0, options=None): - super(SamDB, self).connect(url=self.lp.private_path(url), flags=flags, + if self.lp is not None: + url = self.lp.private_path(url) + + super(SamDB, self).connect(url=url, flags=flags, options=options) + def am_rodc(self): + return dsdb._am_rodc(self) + 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): + return str(self.get_default_basedn()) + + def enable_account(self, search_filter): """Enables an account - - :param filter: LDAP filter to find the user (eg samccountname=name) + + :param search_filter: LDAP filter to find the user (eg + samccountname=name) """ res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE, - expression=filter, attrs=["userAccountControl"]) + expression=search_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): - userAccountControl = userAccountControl & ~0x20 # remove 'no password required' bit + if userAccountControl & 0x2: + # remove disabled bit + userAccountControl = userAccountControl & ~0x2 + if userAccountControl & 0x20: + # remove 'no password required' bit + userAccountControl = userAccountControl & ~0x20 mod = """ dn: %s @@ -88,14 +97,15 @@ replace: userAccountControl userAccountControl: %u """ % (user_dn, userAccountControl) self.modify_ldif(mod) - - def force_password_change_at_next_login(self, filter): + + def force_password_change_at_next_login(self, search_filter): """Forces a password change at next login - - :param filter: LDAP filter to find the user (eg samccountname=name) + + :param search_filter: LDAP filter to find the user (eg + samccountname=name) """ res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE, - expression=filter, attrs=[]) + expression=search_filter, attrs=[]) assert(len(res) == 1) user_dn = res[0].dn @@ -107,102 +117,297 @@ 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 newgroup(self, groupname, groupou=None, grouptype=None, + description=None, mailaddress=None, notes=None, sd=None): + """Adds a new group with additional parameters + + :param groupname: Name of the new group + :param grouptype: Type of the new group + :param description: Description of the new group + :param mailaddress: Email address of the new group + :param notes: Notes of the new group + :param sd: security descriptor of the object + """ + + group_dn = "CN=%s,%s,%s" % (groupname, (groupou or "CN=Users"), self.domain_dn()) + + # The new user record. Note the reliance on the SAMLDB module which + # fills in the default informations + ldbmessage = {"dn": group_dn, + "sAMAccountName": groupname, + "objectClass": "group"} + + if grouptype is not None: + ldbmessage["groupType"] = "%d" % grouptype + + if description is not None: + ldbmessage["description"] = description + + if mailaddress is not None: + ldbmessage["mail"] = mailaddress + + if notes is not None: + ldbmessage["info"] = notes + + if sd is not None: + ldbmessage["nTSecurityDescriptor"] = ndr_pack(sd) + + self.add(ldbmessage) + + def deletegroup(self, groupname): + """Deletes a group + + :param groupname: Name of the target group + """ + + groupfilter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (groupname, "CN=Group,CN=Schema,CN=Configuration", self.domain_dn()) + self.transaction_start() + try: + targetgroup = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE, + expression=groupfilter, attrs=[]) + if len(targetgroup) == 0: + raise Exception('Unable to find group "%s"' % groupname) + assert(len(targetgroup) == 1) + self.delete(targetgroup[0].dn) + except Exception: + self.transaction_cancel() + raise + else: + self.transaction_commit() + + def add_remove_group_members(self, groupname, listofmembers, + add_members_operation=True): + """Adds or removes group members + + :param groupname: Name of the target group + :param listofmembers: Comma-separated list of group members + :param add_members_operation: Defines if its an add or remove + operation + """ + + groupfilter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (groupname, "CN=Group,CN=Schema,CN=Configuration", self.domain_dn()) + groupmembers = listofmembers.split(',') + + self.transaction_start() + try: + targetgroup = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE, + expression=groupfilter, attrs=['member']) + if len(targetgroup) == 0: + raise Exception('Unable to find group "%s"' % groupname) + assert(len(targetgroup) == 1) + + modified = False + + addtargettogroup = """ +dn: %s +changetype: modify +""" % (str(targetgroup[0].dn)) + + for member in groupmembers: + targetmember = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE, + expression="(|(sAMAccountName=%s)(CN=%s))" % (member, member), attrs=[]) + + if len(targetmember) != 1: + continue + + if add_members_operation is True and (targetgroup[0].get('member') is None or str(targetmember[0].dn) not in targetgroup[0]['member']): + modified = True + addtargettogroup += """add: member +member: %s +""" % (str(targetmember[0].dn)) + + elif add_members_operation is False and (targetgroup[0].get('member') is not None and str(targetmember[0].dn) in targetgroup[0]['member']): + modified = True + addtargettogroup += """delete: member +member: %s +""" % (str(targetmember[0].dn)) + + if modified is True: + self.modify_ldif(addtargettogroup) + + except Exception: + self.transaction_cancel() + raise + else: + self.transaction_commit() + + def newuser(self, username, password, + force_password_change_at_next_login_req=False, + useusernameascn=False, userou=None, surname=None, givenname=None, + initials=None, profilepath=None, scriptpath=None, homedrive=None, + homedirectory=None, jobtitle=None, department=None, company=None, + description=None, mailaddress=None, internetaddress=None, + telephonenumber=None, physicaldeliveryoffice=None, sd=None, + setpassword=True): + """Adds a new user with additional parameters - 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 password: Password for the new user :param force_password_change_at_next_login_req: Force password change + :param useusernameascn: Use username as cn rather that firstname + + initials + lastname + :param userou: Object container (without domainDN postfix) for new user + :param surname: Surname of the new user + :param givenname: First name of the new user + :param initials: Initials of the new user + :param profilepath: Profile path of the new user + :param scriptpath: Logon script path of the new user + :param homedrive: Home drive of the new user + :param homedirectory: Home directory of the new user + :param jobtitle: Job title of the new user + :param department: Department of the new user + :param company: Company of the new user + :param description: of the new user + :param mailaddress: Email address of the new user + :param internetaddress: Home page of the new user + :param telephonenumber: Phone number of the new user + :param physicaldeliveryoffice: Office location of the new user + :param sd: security descriptor of the object + :param setpassword: optionally disable password reset """ + + displayname = "" + if givenname is not None: + displayname += givenname + + if initials is not None: + displayname += ' %s.' % initials + + if surname is not None: + displayname += ' %s' % surname + + cn = username + if useusernameascn is None and displayname is not "": + cn = displayname + + user_dn = "CN=%s,%s,%s" % (cn, (userou or "CN=Users"), self.domain_dn()) + + dnsdomain = ldb.Dn(self, self.domain_dn()).canonical_str().replace("/", "") + user_principal_name = "%s@%s" % (username, dnsdomain) + # The new user record. Note the reliance on the SAMLDB module which + # fills in the default informations + ldbmessage = {"dn": user_dn, + "sAMAccountName": username, + "userPrincipalName": user_principal_name, + "objectClass": "user"} + + if surname is not None: + ldbmessage["sn"] = surname + + if givenname is not None: + ldbmessage["givenName"] = givenname + + if displayname is not "": + ldbmessage["displayName"] = displayname + ldbmessage["name"] = displayname + + if initials is not None: + ldbmessage["initials"] = '%s.' % initials + + if profilepath is not None: + ldbmessage["profilePath"] = profilepath + + if scriptpath is not None: + ldbmessage["scriptPath"] = scriptpath + + if homedrive is not None: + ldbmessage["homeDrive"] = homedrive + + if homedirectory is not None: + ldbmessage["homeDirectory"] = homedirectory + + if jobtitle is not None: + ldbmessage["title"] = jobtitle + + if department is not None: + ldbmessage["department"] = department + + if company is not None: + ldbmessage["company"] = company + + if description is not None: + ldbmessage["description"] = description + + if mailaddress is not None: + ldbmessage["mail"] = mailaddress + + if internetaddress is not None: + ldbmessage["wWWHomePage"] = internetaddress + + if telephonenumber is not None: + ldbmessage["telephoneNumber"] = telephonenumber + + if physicaldeliveryoffice is not None: + ldbmessage["physicalDeliveryOfficeName"] = physicaldeliveryoffice + + if sd is not None: + ldbmessage["nTSecurityDescriptor"] = ndr_pack(sd) + self.transaction_start() try: - 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, - "objectClass": "user"}) + self.add(ldbmessage) # 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"]) - assert len(res) == 1 - user_sid = self.schema_format_value("objectSid", res[0]["objectSid"][0]) - - try: - 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 - except: + if setpassword: + self.setpassword("(samAccountName=%s)" % username, password, + force_password_change_at_next_login_req) + except Exception: self.transaction_cancel() raise - self.transaction_commit() + else: + self.transaction_commit() - def setpassword(self, filter, password, force_password_change_at_next_login_req=False): + def setpassword(self, search_filter, password, + force_change_at_next_login=False, username=None): """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 search_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 + :param force_change_at_next_login: Force password change """ self.transaction_start() try: res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE, - expression=filter, attrs=[]) - assert(len(res) == 1) + expression=search_filter, attrs=[]) + if len(res) == 0: + raise Exception('Unable to find user "%s"' % (username or search_filter)) + if len(res) > 1: + raise Exception('Matched %u multiple users with filter "%s"' % (len(res), search_filter)) user_dn = res[0].dn - setpw = """ dn: %s changetype: modify -replace: userPassword -userPassword:: %s -""" % (user_dn, base64.b64encode(password)) +replace: unicodePwd +unicodePwd:: %s +""" % (user_dn, base64.b64encode(("\"" + password + "\"").encode('utf-16-le'))) self.modify_ldif(setpw) - if force_password_change_at_next_login_req: + if force_change_at_next_login: self.force_password_change_at_next_login( "(dn=" + str(user_dn) + ")") # modify the userAccountControl to remove the disabled bit - self.enable_account(filter) - except: + self.enable_account(search_filter) + except Exception: self.transaction_cancel() raise - self.transaction_commit() + else: + self.transaction_commit() - def setexpiry(self, filter, expiry_seconds, no_expiry_req=False): + def setexpiry(self, search_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 search_filter: LDAP filter to find the user (eg + samaccountname=name) :param expiry_seconds: expiry time from now in seconds :param no_expiry_req: if set, then don't expire password """ self.transaction_start() try: res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE, - expression=filter, + expression=search_filter, attrs=["userAccountControl", "accountExpires"]) assert(len(res) == 1) user_dn = res[0].dn @@ -214,7 +419,7 @@ userPassword:: %s accountExpires = 0 else: userAccountControl = userAccountControl & ~0x10000 - accountExpires = glue.unix2nttime(expiry_seconds + int(time.time())) + accountExpires = samba.unix2nttime(expiry_seconds + int(time.time())) setexp = """ dn: %s @@ -226,8 +431,283 @@ accountExpires: %u """ % (user_dn, userAccountControl, accountExpires) self.modify_ldif(setexp) - except: + except Exception: self.transaction_cancel() raise - self.transaction_commit(); + else: + self.transaction_commit() + + def set_domain_sid(self, sid): + """Change the domain SID used by this LDB. + + :param sid: The new domain sid to use. + """ + dsdb._samdb_set_domain_sid(self, sid) + + def get_domain_sid(self): + """Read the domain SID used by this LDB. """ + return dsdb._samdb_get_domain_sid(self) + + domain_sid = property(get_domain_sid, set_domain_sid, + "SID for the domain") + + def set_invocation_id(self, invocation_id): + """Set the invocation id for this SamDB handle. + + :param invocation_id: GUID of the invocation id. + """ + dsdb._dsdb_set_ntds_invocation_id(self, invocation_id) + + def get_invocation_id(self): + """Get the invocation_id id""" + return dsdb._samdb_ntds_invocation_id(self) + + invocation_id = property(get_invocation_id, set_invocation_id, + "Invocation ID GUID") + + def get_oid_from_attid(self, attid): + return dsdb._dsdb_get_oid_from_attid(self, attid) + + def get_attid_from_lDAPDisplayName(self, ldap_display_name, + is_schema_nc=False): + return dsdb._dsdb_get_attid_from_lDAPDisplayName(self, + ldap_display_name, is_schema_nc) + + def set_ntds_settings_dn(self, ntds_settings_dn): + """Set the NTDS Settings DN, as would be returned on the dsServiceName + rootDSE attribute. + + This allows the DN to be set before the database fully exists + + :param ntds_settings_dn: The new DN to use + """ + dsdb._samdb_set_ntds_settings_dn(self, ntds_settings_dn) + + def get_ntds_GUID(self): + """Get the NTDS objectGUID""" + return dsdb._samdb_ntds_objectGUID(self) + + def server_site_name(self): + """Get the server site name""" + return dsdb._samdb_server_site_name(self) + + def load_partition_usn(self, base_dn): + return dsdb._dsdb_load_partition_usn(self, base_dn) + + def set_schema(self, schema): + self.set_schema_from_ldb(schema.ldb) + + def set_schema_from_ldb(self, ldb_conn): + dsdb._dsdb_set_schema_from_ldb(self, ldb_conn) + + def dsdb_DsReplicaAttribute(self, ldb, ldap_display_name, ldif_elements): + return dsdb._dsdb_DsReplicaAttribute(ldb, ldap_display_name, ldif_elements) + + def get_attribute_from_attid(self, attid): + """ Get from an attid the associated attribute + + :param attid: The attribute id for searched attribute + :return: The name of the attribute associated with this id + """ + if len(self.hash_oid_name.keys()) == 0: + self._populate_oid_attid() + if self.hash_oid_name.has_key(self.get_oid_from_attid(attid)): + return self.hash_oid_name[self.get_oid_from_attid(attid)] + else: + return None + + def _populate_oid_attid(self): + """Populate the hash hash_oid_name. + + This hash contains the oid of the attribute as a key and + its display name as a value + """ + self.hash_oid_name = {} + res = self.search(expression="objectClass=attributeSchema", + controls=["search_options:1:2"], + attrs=["attributeID", + "lDAPDisplayName"]) + if len(res) > 0: + for e in res: + strDisplay = str(e.get("lDAPDisplayName")) + self.hash_oid_name[str(e.get("attributeID"))] = strDisplay + + def get_attribute_replmetadata_version(self, dn, att): + """Get the version field trom the replPropertyMetaData for + the given field + + :param dn: The on which we want to get the version + :param att: The name of the attribute + :return: The value of the version field in the replPropertyMetaData + for the given attribute. None if the attribute is not replicated + """ + + res = self.search(expression="dn=%s" % dn, + scope=ldb.SCOPE_SUBTREE, + controls=["search_options:1:2"], + attrs=["replPropertyMetaData"]) + if len(res) == 0: + return None + + repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob, + str(res[0]["replPropertyMetaData"])) + ctr = repl.ctr + if len(self.hash_oid_name.keys()) == 0: + self._populate_oid_attid() + for o in ctr.array: + # Search for Description + att_oid = self.get_oid_from_attid(o.attid) + if self.hash_oid_name.has_key(att_oid) and\ + att.lower() == self.hash_oid_name[att_oid].lower(): + return o.version + return None + + def set_attribute_replmetadata_version(self, dn, att, value, + addifnotexist=False): + res = self.search(expression="dn=%s" % dn, + scope=ldb.SCOPE_SUBTREE, + controls=["search_options:1:2"], + attrs=["replPropertyMetaData"]) + if len(res) == 0: + return None + + repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob, + str(res[0]["replPropertyMetaData"])) + ctr = repl.ctr + now = samba.unix2nttime(int(time.time())) + found = False + if len(self.hash_oid_name.keys()) == 0: + self._populate_oid_attid() + for o in ctr.array: + # Search for Description + att_oid = self.get_oid_from_attid(o.attid) + if self.hash_oid_name.has_key(att_oid) and\ + att.lower() == self.hash_oid_name[att_oid].lower(): + found = True + seq = self.sequence_number(ldb.SEQ_NEXT) + o.version = value + o.originating_change_time = now + o.originating_invocation_id = misc.GUID(self.get_invocation_id()) + o.originating_usn = seq + o.local_usn = seq + + if not found and addifnotexist and len(ctr.array) >0: + o2 = drsblobs.replPropertyMetaData1() + o2.attid = 589914 + att_oid = self.get_oid_from_attid(o2.attid) + seq = self.sequence_number(ldb.SEQ_NEXT) + o2.version = value + o2.originating_change_time = now + o2.originating_invocation_id = misc.GUID(self.get_invocation_id()) + o2.originating_usn = seq + o2.local_usn = seq + found = True + tab = ctr.array + tab.append(o2) + ctr.count = ctr.count + 1 + ctr.array = tab + + if found : + replBlob = ndr_pack(repl) + msg = ldb.Message() + msg.dn = res[0].dn + msg["replPropertyMetaData"] = ldb.MessageElement(replBlob, + ldb.FLAG_MOD_REPLACE, + "replPropertyMetaData") + self.modify(msg, ["local_oid:1.3.6.1.4.1.7165.4.3.14:0"]) + + def write_prefixes_from_schema(self): + dsdb._dsdb_write_prefixes_from_schema_to_ldb(self) + + def get_partitions_dn(self): + return dsdb._dsdb_get_partitions_dn(self) + + def set_minPwdAge(self, value): + m = ldb.Message() + m.dn = ldb.Dn(self, self.domain_dn()) + m["minPwdAge"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "minPwdAge") + self.modify(m) + + def get_minPwdAge(self): + res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["minPwdAge"]) + if len(res) == 0: + return None + elif not "minPwdAge" in res[0]: + return None + else: + return res[0]["minPwdAge"][0] + + def set_minPwdLength(self, value): + m = ldb.Message() + m.dn = ldb.Dn(self, self.domain_dn()) + m["minPwdLength"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "minPwdLength") + self.modify(m) + + def get_minPwdLength(self): + res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["minPwdLength"]) + if len(res) == 0: + return None + elif not "minPwdLength" in res[0]: + return None + else: + return res[0]["minPwdLength"][0] + + def set_pwdProperties(self, value): + m = ldb.Message() + m.dn = ldb.Dn(self, self.domain_dn()) + m["pwdProperties"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "pwdProperties") + self.modify(m) + + def get_pwdProperties(self): + res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["pwdProperties"]) + if len(res) == 0: + return None + elif not "pwdProperties" in res[0]: + return None + else: + return res[0]["pwdProperties"][0] + + def set_dsheuristics(self, dsheuristics): + m = ldb.Message() + m.dn = ldb.Dn(self, "CN=Directory Service,CN=Windows NT,CN=Services,%s" + % self.get_config_basedn().get_linearized()) + if dsheuristics is not None: + m["dSHeuristics"] = ldb.MessageElement(dsheuristics, + ldb.FLAG_MOD_REPLACE, "dSHeuristics") + else: + m["dSHeuristics"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE, + "dSHeuristics") + self.modify(m) + + def get_dsheuristics(self): + res = self.search("CN=Directory Service,CN=Windows NT,CN=Services,%s" + % self.get_config_basedn().get_linearized(), + scope=ldb.SCOPE_BASE, attrs=["dSHeuristics"]) + if len(res) == 0: + dsheuristics = None + elif "dSHeuristics" in res[0]: + dsheuristics = res[0]["dSHeuristics"][0] + else: + dsheuristics = None + + return dsheuristics + + def create_ou(self, ou_dn, description=None, name=None, sd=None): + """Creates an organizationalUnit object + :param ou_dn: dn of the new object + :param description: description attribute + :param name: name atttribute + :param sd: security descriptor of the object, can be + an SDDL string or security.descriptor type + """ + m = {"dn": ou_dn, + "objectClass": "organizationalUnit"} + + if description: + m["description"] = description + if name: + m["name"] = name + if sd: + m["nTSecurityDescriptor"] = ndr_pack(sd) + self.add(m) diff --git a/source4/scripting/python/samba/schema.py b/source4/scripting/python/samba/schema.py new file mode 100644 index 0000000000..8bac26e24f --- /dev/null +++ b/source4/scripting/python/samba/schema.py @@ -0,0 +1,204 @@ +# +# Unix SMB/CIFS implementation. +# backend code for provisioning a Samba4 server +# +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2008 +# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2008-2009 +# Copyright (C) Oliver Liebel <oliver@itc.li> 2008-2009 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +"""Functions for setting up a Samba Schema.""" + +from base64 import b64encode +from samba import read_and_sub_file, substitute_var, check_all_substituted +from samba.dcerpc import security +from samba.ms_schema import read_ms_schema +from samba.ndr import ndr_pack +from samba.samdb import SamDB +from samba import dsdb +from ldb import SCOPE_SUBTREE, SCOPE_ONELEVEL +import os + +def get_schema_descriptor(domain_sid): + sddl = "O:SAG:SAD:AI(OA;;CR;e12b56b6-0a95-11d1-adbb-00c04fd8d5cd;;SA)" \ + "(OA;;CR;1131f6aa-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \ + "(OA;;CR;1131f6ab-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \ + "(OA;;CR;1131f6ac-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \ + "(OA;;CR;1131f6aa-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(OA;;CR;1131f6ab-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(OA;;CR;1131f6ac-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(A;CI;RPLCLORC;;;AU)" \ + "(A;CI;RPWPCRCCLCLORCWOWDSW;;;SA)" \ + "(A;CI;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)" \ + "(OA;;CR;1131f6ad-9c07-11d1-f79f-00c04fc2dcd2;;ED)" \ + "(OA;;CR;89e95b76-444d-4c62-991a-0facbeda640c;;ED)" \ + "(OA;;CR;1131f6ad-9c07-11d1-f79f-00c04fc2dcd2;;BA)" \ + "(OA;;CR;89e95b76-444d-4c62-991a-0facbeda640c;;BA)" \ + "(OA;;CR;1131f6aa-9c07-11d1-f79f-00c04fc2dcd2;;ER)" \ + "(OA;;CR;1131f6ad-9c07-11d1-f79f-00c04fc2dcd2;;ER)" \ + "(OA;;CR;89e95b76-444d-4c62-991a-0facbeda640c;;ER)" \ + "S:(AU;SA;WPCCDCWOWDSDDTSW;;;WD)" \ + "(AU;CISA;WP;;;WD)" \ + "(AU;SA;CR;;;BA)" \ + "(AU;SA;CR;;;DU)" \ + "(OU;SA;CR;e12b56b6-0a95-11d1-adbb-00c04fd8d5cd;;WD)" \ + "(OU;SA;CR;45ec5156-db7e-47bb-b53f-dbeb2d03c40f;;WD)" + sec = security.descriptor.from_sddl(sddl, domain_sid) + return ndr_pack(sec) + + +class Schema(object): + + def __init__(self, domain_sid, invocationid=None, schemadn=None, + files=None, override_prefixmap=None, additional_prefixmap=None): + from samba.provision import setup_path + + """Load schema for the SamDB from the AD schema files and + samba4_schema.ldif + + :param samdb: Load a schema into a SamDB. + :param schemadn: DN of the schema + + Returns the schema data loaded, to avoid double-parsing when then + needing to add it to the db + """ + + self.schemadn = schemadn + # We need to have the am_rodc=False just to keep some warnings quiet - + # this isn't a real SAM, so it's meaningless. + self.ldb = SamDB(global_schema=False, am_rodc=False) + if invocationid is not None: + self.ldb.set_invocation_id(invocationid) + + self.schema_data = read_ms_schema( + setup_path('ad-schema/MS-AD_Schema_2K8_R2_Attributes.txt'), + setup_path('ad-schema/MS-AD_Schema_2K8_R2_Classes.txt')) + + if files is not None: + for file in files: + self.schema_data += open(file, 'r').read() + + self.schema_data = substitute_var(self.schema_data, + {"SCHEMADN": schemadn}) + check_all_substituted(self.schema_data) + + self.schema_dn_modify = read_and_sub_file( + setup_path("provision_schema_basedn_modify.ldif"), + {"SCHEMADN": schemadn}) + + descr = b64encode(get_schema_descriptor(domain_sid)) + self.schema_dn_add = read_and_sub_file( + setup_path("provision_schema_basedn.ldif"), + {"SCHEMADN": schemadn, "DESCRIPTOR": descr}) + + if override_prefixmap is not None: + self.prefixmap_data = override_prefixmap + else: + self.prefixmap_data = open(setup_path("prefixMap.txt"), 'r').read() + + if additional_prefixmap is not None: + for map in additional_prefixmap: + self.prefixmap_data += "%s\n" % map + + self.prefixmap_data = b64encode(self.prefixmap_data) + + # We don't actually add this ldif, just parse it + prefixmap_ldif = "dn: cn=schema\nprefixMap:: %s\n\n" % self.prefixmap_data + self.set_from_ldif(prefixmap_ldif, self.schema_data) + + def set_from_ldif(self, pf, df): + dsdb._dsdb_set_schema_from_ldif(self.ldb, pf, df) + + def write_to_tmp_ldb(self, schemadb_path): + self.ldb.connect(url=schemadb_path) + self.ldb.transaction_start() + try: + self.ldb.add_ldif("""dn: @ATTRIBUTES +linkID: INTEGER + +dn: @INDEXLIST +@IDXATTR: linkID +@IDXATTR: attributeSyntax +""") + # These bits of LDIF are supplied when the Schema object is created + self.ldb.add_ldif(self.schema_dn_add) + self.ldb.modify_ldif(self.schema_dn_modify) + self.ldb.add_ldif(self.schema_data) + except Exception: + self.ldb.transaction_cancel() + raise + else: + self.ldb.transaction_commit() + + # Return a hash with the forward attribute as a key and the back as the + # value + def linked_attributes(self): + return get_linked_attributes(self.schemadn, self.ldb) + + def dnsyntax_attributes(self): + return get_dnsyntax_attributes(self.schemadn, self.ldb) + + def convert_to_openldap(self, target, mapping): + return dsdb._dsdb_convert_schema_to_openldap(self.ldb, target, mapping) + + +# Return a hash with the forward attribute as a key and the back as the value +def get_linked_attributes(schemadn,schemaldb): + attrs = ["linkID", "lDAPDisplayName"] + res = schemaldb.search(expression="(&(linkID=*)(!(linkID:1.2.840.113556.1.4.803:=1))(objectclass=attributeSchema)(attributeSyntax=2.5.5.1))", base=schemadn, scope=SCOPE_ONELEVEL, attrs=attrs) + attributes = {} + for i in range (0, len(res)): + expression = "(&(objectclass=attributeSchema)(linkID=%d)(attributeSyntax=2.5.5.1))" % (int(res[i]["linkID"][0])+1) + target = schemaldb.searchone(basedn=schemadn, + expression=expression, + attribute="lDAPDisplayName", + scope=SCOPE_SUBTREE) + if target is not None: + attributes[str(res[i]["lDAPDisplayName"])]=str(target) + + return attributes + + +def get_dnsyntax_attributes(schemadn,schemaldb): + res = schemaldb.search( + expression="(&(!(linkID=*))(objectclass=attributeSchema)(attributeSyntax=2.5.5.1))", + base=schemadn, scope=SCOPE_ONELEVEL, + attrs=["linkID", "lDAPDisplayName"]) + attributes = [] + for i in range (0, len(res)): + attributes.append(str(res[i]["lDAPDisplayName"])) + return attributes + + +def ldb_with_schema(schemadn="cn=schema,cn=configuration,dc=example,dc=com", + domainsid=None, + override_prefixmap=None): + """Load schema for the SamDB from the AD schema files and samba4_schema.ldif + + :param schemadn: DN of the schema + :param serverdn: DN of the server + + Returns the schema data loaded as an object, with .ldb being a + new ldb with the schema loaded. This allows certain tests to + operate without a remote or local schema. + """ + + if domainsid is None: + domainsid = security.random_sid() + else: + domainsid = security.dom_sid(domainsid) + return Schema(domainsid, schemadn=schemadn, + override_prefixmap=override_prefixmap) diff --git a/source4/scripting/python/samba/sd_utils.py b/source4/scripting/python/samba/sd_utils.py new file mode 100644 index 0000000000..4694f4cc76 --- /dev/null +++ b/source4/scripting/python/samba/sd_utils.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +# +# Utility methods for security descriptor manipulation +# +# Copyright Nadezhda Ivanova 2010 <nivanova@samba.org> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import samba +from ldb import Message, MessageElement, Dn +from ldb import FLAG_MOD_REPLACE, SCOPE_BASE +from samba.ndr import ndr_pack, ndr_unpack +from samba.dcerpc import security + +class SDUtils: + '''Some utilities for manipulation of security descriptors + on objects''' + + def __init__(self, samdb): + self.ldb = samdb + self.domain_sid = security.dom_sid(self.ldb.get_domain_sid()) + + def modify_sd_on_dn(self, object_dn, sd, controls=None): + """ Modify security descriptor using either SDDL string + or security.descriptor object + """ + m = Message() + m.dn = Dn(self.ldb, object_dn) + assert(isinstance(sd, str) or isinstance(sd, security.descriptor)) + if isinstance(sd, str): + tmp_desc = security.descriptor.from_sddl(sd, self.domain_sid) + elif isinstance(sd, security.descriptor): + tmp_desc = sd + + m["nTSecurityDescriptor"] = MessageElement(ndr_pack(tmp_desc), + FLAG_MOD_REPLACE, + "nTSecurityDescriptor") + self.ldb.modify(m, controls) + + def read_sd_on_dn(self, object_dn, controls=None): + res = self.ldb.search(object_dn, SCOPE_BASE, None, + ["nTSecurityDescriptor"], controls=controls) + desc = res[0]["nTSecurityDescriptor"][0] + return ndr_unpack(security.descriptor, desc) + + def get_object_sid(self, object_dn): + res = self.ldb.search(object_dn) + return ndr_unpack(security.dom_sid, res[0]["objectSid"][0]) + + def dacl_add_ace(self, object_dn, ace): + """ Adds an ACE to an objects security descriptor + """ + desc = self.read_sd_on_dn(object_dn) + desc_sddl = desc.as_sddl(self.domain_sid) + if ace in desc_sddl: + return + if desc_sddl.find("(") >= 0: + desc_sddl = desc_sddl[:desc_sddl.index("(")] + ace + desc_sddl[desc_sddl.index("("):] + else: + desc_sddl = desc_sddl + ace + self.modify_sd_on_dn(object_dn, desc_sddl) + + def get_sd_as_sddl(self, object_dn, controls=None): + """ Return object nTSecutiryDescriptor in SDDL format + """ + desc = self.read_sd_on_dn(object_dn, controls=controls) + return desc.as_sddl(self.domain_sid) diff --git a/source4/scripting/python/samba/shares.py b/source4/scripting/python/samba/shares.py deleted file mode 100644 index 89a312e855..0000000000 --- a/source4/scripting/python/samba/shares.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/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 ae7a707e35..58e4130998 100644 --- a/source4/scripting/python/samba/tests/__init__.py +++ b/source4/scripting/python/samba/tests/__init__.py @@ -1,18 +1,18 @@ -#!/usr/bin/python +#!/usr/bin/env python # Unix SMB/CIFS implementation. -# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2008 -# +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2010 +# # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. -# +# # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # @@ -22,13 +22,43 @@ import os import ldb import samba +import samba.auth +from samba import param +from samba.samdb import SamDB +import subprocess import tempfile -import unittest -class LdbTestCase(unittest.TestCase): +# Other modules import these two classes from here, for convenience: +from testtools.testcase import ( + TestCase as TesttoolsTestCase, + TestSkipped, + ) + + +class TestCase(TesttoolsTestCase): + """A Samba test case.""" + + def setUp(self): + super(TestCase, self).setUp() + test_debug_level = os.getenv("TEST_DEBUG_LEVEL") + if test_debug_level is not None: + test_debug_level = int(test_debug_level) + self._old_debug_level = samba.get_debug_level() + samba.set_debug_level(test_debug_level) + self.addCleanup(samba.set_debug_level, test_debug_level) + + def get_loadparm(self): + return env_loadparm() + + def get_credentials(self): + return cmdline_credentials + +class LdbTestCase(TesttoolsTestCase): """Trivial test case for running tests against a LDB.""" + def setUp(self): + super(LdbTestCase, self).setUp() self.filename = os.tempnam() self.ldb = samba.Ldb(self.filename) @@ -41,7 +71,7 @@ class LdbTestCase(unittest.TestCase): self.ldb = samba.Ldb(self.filename) -class TestCaseInTempDir(unittest.TestCase): +class TestCaseInTempDir(TestCase): def setUp(self): super(TestCaseInTempDir, self).setUp() @@ -53,57 +83,33 @@ class TestCaseInTempDir(unittest.TestCase): os.rmdir(self.tempdir) -class SubstituteVarTestCase(unittest.TestCase): - - def test_empty(self): - self.assertEquals("", samba.substitute_var("", {})) - - def test_nothing(self): - self.assertEquals("foo bar", samba.substitute_var("foo bar", {"bar": "bla"})) - - def test_replace(self): - self.assertEquals("foo bla", samba.substitute_var("foo ${bar}", {"bar": "bla"})) +def env_loadparm(): + lp = param.LoadParm() + try: + lp.load(os.environ["SMB_CONF_PATH"]) + except KeyError: + raise Exception("SMB_CONF_PATH not set") + return lp - def test_broken(self): - self.assertEquals("foo ${bdkjfhsdkfh sdkfh ", - samba.substitute_var("foo ${bdkjfhsdkfh sdkfh ", {"bar": "bla"})) - def test_unknown_var(self): - self.assertEquals("foo ${bla} gsff", - samba.substitute_var("foo ${bla} gsff", {"bar": "bla"})) - - def test_check_all_substituted(self): - samba.check_all_substituted("nothing to see here") - self.assertRaises(Exception, samba.check_all_substituted, "Not subsituted: ${FOOBAR}") +def env_get_var_value(var_name): + """Returns value for variable in os.environ + Function throws AssertionError if variable is defined. + Unit-test based python tests require certain input params + to be set in environment, otherwise they can't be run + """ + assert var_name in os.environ.keys(), "Please supply %s in environment" % var_name + return os.environ[var_name] -class LdbExtensionTests(TestCaseInTempDir): - def test_searchone(self): - path = self.tempdir + "/searchone.ldb" - l = samba.Ldb(path) - try: - l.add({"dn": "foo=dc", "bar": "bla"}) - self.assertEquals("bla", l.searchone(basedn=ldb.Dn(l, "foo=dc"), attribute="bar")) - finally: - del l - os.unlink(path) - - -cmdline_loadparm = None cmdline_credentials = None -class RpcInterfaceTestCase(unittest.TestCase): +class RpcInterfaceTestCase(TestCase): + """DCE/RPC Test case.""" - def get_loadparm(self): - assert cmdline_loadparm is not None - return cmdline_loadparm - - def get_credentials(self): - return cmdline_credentials - -class ValidNetbiosNameTests(unittest.TestCase): +class ValidNetbiosNameTests(TestCase): def test_valid(self): self.assertTrue(samba.valid_netbios_name("FOO")) @@ -113,3 +119,110 @@ class ValidNetbiosNameTests(unittest.TestCase): def test_invalid_characters(self): self.assertFalse(samba.valid_netbios_name("*BLA")) + + +class BlackboxProcessError(subprocess.CalledProcessError): + """This exception is raised when a process run by check_output() returns + a non-zero exit status. Exception instance should contain + the exact exit code (S.returncode), command line (S.cmd), + process output (S.stdout) and process error stream (S.stderr)""" + def __init__(self, returncode, cmd, stdout, stderr): + super(BlackboxProcessError, self).__init__(returncode, cmd) + self.stdout = stdout + self.stderr = stderr + def __str__(self): + return "Command '%s'; exit status %d; stdout: '%s'; stderr: '%s'" % (self.cmd, self.returncode, + self.stdout, self.stderr) + +class BlackboxTestCase(TestCase): + """Base test case for blackbox tests.""" + + def _make_cmdline(self, line): + bindir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../../bin")) + parts = line.split(" ") + if os.path.exists(os.path.join(bindir, parts[0])): + parts[0] = os.path.join(bindir, parts[0]) + line = " ".join(parts) + return line + + def check_run(self, line): + line = self._make_cmdline(line) + subprocess.check_call(line, shell=True) + + def check_output(self, line): + line = self._make_cmdline(line) + p = subprocess.Popen(line, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, close_fds=True) + retcode = p.wait() + if retcode: + raise BlackboxProcessError(retcode, line, p.stdout.read(), p.stderr.read()) + return p.stdout.read() + +def connect_samdb(samdb_url, lp=None, session_info=None, credentials=None, + flags=0, ldb_options=None, ldap_only=False): + """Create SamDB instance and connects to samdb_url database. + + :param samdb_url: Url for database to connect to. + :param lp: Optional loadparm object + :param session_info: Optional session information + :param credentials: Optional credentials, defaults to anonymous. + :param flags: Optional LDB flags + :param ldap_only: If set, only remote LDAP connection will be created. + + Added value for tests is that we have a shorthand function + to make proper URL for ldb.connect() while using default + parameters for connection based on test environment + """ + samdb_url = samdb_url.lower() + if not "://" in samdb_url: + if not ldap_only and os.path.isfile(samdb_url): + samdb_url = "tdb://%s" % samdb_url + else: + samdb_url = "ldap://%s" % samdb_url + # use 'paged_search' module when connecting remotely + if samdb_url.startswith("ldap://"): + ldb_options = ["modules:paged_searches"] + elif ldap_only: + raise AssertionError("Trying to connect to %s while remote " + "connection is required" % samdb_url) + + # set defaults for test environment + if lp is None: + lp = env_loadparm() + if session_info is None: + session_info = samba.auth.system_session(lp) + if credentials is None: + credentials = cmdline_credentials + + return SamDB(url=samdb_url, + lp=lp, + session_info=session_info, + credentials=credentials, + flags=flags, + options=ldb_options) + + +def connect_samdb_ex(samdb_url, lp=None, session_info=None, credentials=None, + flags=0, ldb_options=None, ldap_only=False): + """Connects to samdb_url database + + :param samdb_url: Url for database to connect to. + :param lp: Optional loadparm object + :param session_info: Optional session information + :param credentials: Optional credentials, defaults to anonymous. + :param flags: Optional LDB flags + :param ldap_only: If set, only remote LDAP connection will be created. + :return: (sam_db_connection, rootDse_record) tuple + """ + sam_db = connect_samdb(samdb_url, lp, session_info, credentials, + flags, ldb_options, ldap_only) + # fetch RootDse + res = sam_db.search(base="", expression="", scope=ldb.SCOPE_BASE, + attrs=["*"]) + return (sam_db, res[0]) + + +def delete_force(samdb, dn): + try: + samdb.delete(dn) + except ldb.LdbError, (num, _): + assert(num == ldb.ERR_NO_SUCH_OBJECT) diff --git a/source4/scripting/python/samba/tests/auth.py b/source4/scripting/python/samba/tests/auth.py new file mode 100644 index 0000000000..6ecfc2047f --- /dev/null +++ b/source4/scripting/python/samba/tests/auth.py @@ -0,0 +1,33 @@ +#!/usr/bin/env 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/>. +# + +"""Tests for the Auth Python bindings. + +Note that this just tests the bindings work. It does not intend to test +the functionality, that's already done in other tests. +""" + +from samba import auth +import samba.tests + +class AuthTests(samba.tests.TestCase): + + def test_system_session(self): + auth.system_session() + diff --git a/source4/scripting/python/samba/tests/blackbox/__init__.py b/source4/scripting/python/samba/tests/blackbox/__init__.py new file mode 100644 index 0000000000..8569cb55bd --- /dev/null +++ b/source4/scripting/python/samba/tests/blackbox/__init__.py @@ -0,0 +1 @@ +"""Blackbox tests. """ diff --git a/source4/scripting/python/samba/tests/blackbox/ndrdump.py b/source4/scripting/python/samba/tests/blackbox/ndrdump.py new file mode 100755 index 0000000000..c07e32a24f --- /dev/null +++ b/source4/scripting/python/samba/tests/blackbox/ndrdump.py @@ -0,0 +1,36 @@ +#!/usr/bin/python +# Blackbox tests for ndrdump +# Copyright (C) 2008 Andrew Tridgell <tridge@samba.org> +# Copyright (C) 2008 Andrew Bartlett <abartlet@samba.org> +# Copyright (C) 2010 Jelmer Vernooij <jelmer@samba.org> +# based on test_smbclient.sh + +"""Blackbox tests for ndrdump.""" + +import os +from samba.tests import BlackboxTestCase + +for p in [ "../../../../../source4/librpc/tests", "../../../../../librpc/tests"]: + data_path_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), p)) + print data_path_dir + if os.path.exists(data_path_dir): + break + + +class NdrDumpTests(BlackboxTestCase): + """Blackbox tests for ndrdump.""" + + def data_path(self, name): + return os.path.join(data_path_dir, name) + + def test_ndrdump_with_in(self): + self.check_run("ndrdump samr samr_CreateUser in %s" % (self.data_path("samr-CreateUser-in.dat"))) + + def test_ndrdump_with_out(self): + self.check_run("ndrdump samr samr_CreateUser out %s" % (self.data_path("samr-CreateUser-out.dat"))) + + def test_ndrdump_context_file(self): + self.check_run("ndrdump --context-file %s samr samr_CreateUser out %s" % (self.data_path("samr-CreateUser-in.dat"), self.data_path("samr-CreateUser-out.dat"))) + + def test_ndrdump_with_validate(self): + self.check_run("ndrdump --validate samr samr_CreateUser in %s" % (self.data_path("samr-CreateUser-in.dat"))) diff --git a/source4/scripting/python/samba/tests/blackbox/samba_tool_drs.py b/source4/scripting/python/samba/tests/blackbox/samba_tool_drs.py new file mode 100644 index 0000000000..51274cc9bd --- /dev/null +++ b/source4/scripting/python/samba/tests/blackbox/samba_tool_drs.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python + +# Blackbox tests for "samba-tool drs" command +# Copyright (C) Kamen Mazdrashki <kamenim@samba.org> 2011 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +"""Blackbox tests for samba-tool drs.""" + +import os +import samba.tests + + +class SambaToolDrsTests(samba.tests.BlackboxTestCase): + """Blackbox test case for samba-tool drs.""" + + def setUp(self): + super(SambaToolDrsTests, self).setUp() + + self.dc1 = samba.tests.env_get_var_value("DC1") + self.dc2 = samba.tests.env_get_var_value("DC2") + + creds = self.get_credentials() + self.cmdline_creds = "-U%s/%s%%%s" % (creds.get_domain(), + creds.get_username(), creds.get_password()) + + def _get_rootDSE(self, dc): + samdb = samba.tests.connect_samdb(dc, lp=self.get_loadparm(), + credentials=self.get_credentials(), + ldap_only=True) + return samdb.search(base="", scope=samba.tests.ldb.SCOPE_BASE)[0] + + def test_samba_tool_bind(self): + """Tests 'samba-tool drs bind' command + Output should be like: + Extensions supported: + <list-of-supported-extensions> + Site GUID: <GUID> + Repl epoch: 0""" + out = self.check_output("samba-tool drs bind %s %s" % (self.dc1, + self.cmdline_creds)) + self.assertTrue("Site GUID:" in out) + self.assertTrue("Repl epoch:" in out) + + def test_samba_tool_kcc(self): + """Tests 'samba-tool drs kcc' command + Output should be like 'Consistency check on <DC> successful.'""" + out = self.check_output("samba-tool drs kcc %s %s" % (self.dc1, + self.cmdline_creds)) + self.assertTrue("Consistency check on" in out) + self.assertTrue("successful" in out) + + def test_samba_tool_showrepl(self): + """Tests 'samba-tool drs showrepl' command + Output should be like: + <site-name>/<domain-name> + DSA Options: <hex-options> + DSA object GUID: <DSA-object-GUID> + DSA invocationId: <DSA-invocationId> + <Inbound-connections-list> + <Outbound-connections-list> + <KCC-objects> + ... + TODO: Perhaps we should check at least for + DSA's objectGUDI and invocationId""" + out = self.check_output("samba-tool drs showrepl %s %s" % (self.dc1, + self.cmdline_creds)) + self.assertTrue("DSA Options:" in out) + self.assertTrue("DSA object GUID:" in out) + self.assertTrue("DSA invocationId:" in out) + + def test_samba_tool_options(self): + """Tests 'samba-tool drs options' command + Output should be like 'Current DSA options: IS_GC <OTHER_FLAGS>'""" + out = self.check_output("samba-tool drs options %s %s" % (self.dc1, + self.cmdline_creds)) + self.assertTrue("Current DSA options:" in out) + + def test_samba_tool_replicate(self): + """Tests 'samba-tool drs replicate' command + Output should be like 'Replicate from <DC-SRC> to <DC-DEST> was successful.'""" + nc_name = self._get_rootDSE(self.dc1)["defaultNamingContext"] + out = self.check_output("samba-tool drs replicate %s %s %s %s" % (self.dc1, + self.dc2, + nc_name, + self.cmdline_creds)) + self.assertTrue("Replicate from" in out) + self.assertTrue("was successful" in out) diff --git a/source4/scripting/python/samba/tests/core.py b/source4/scripting/python/samba/tests/core.py new file mode 100644 index 0000000000..1c3d7dbe44 --- /dev/null +++ b/source4/scripting/python/samba/tests/core.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python + +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2008 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +"""Samba Python tests.""" + +import ldb +import os +import samba +from samba.tests import TestCase, TestCaseInTempDir + +class SubstituteVarTestCase(TestCase): + + def test_empty(self): + self.assertEquals("", samba.substitute_var("", {})) + + def test_nothing(self): + self.assertEquals("foo bar", + samba.substitute_var("foo bar", {"bar": "bla"})) + + def test_replace(self): + self.assertEquals("foo bla", + samba.substitute_var("foo ${bar}", {"bar": "bla"})) + + def test_broken(self): + self.assertEquals("foo ${bdkjfhsdkfh sdkfh ", + samba.substitute_var("foo ${bdkjfhsdkfh sdkfh ", {"bar": "bla"})) + + def test_unknown_var(self): + self.assertEquals("foo ${bla} gsff", + samba.substitute_var("foo ${bla} gsff", {"bar": "bla"})) + + def test_check_all_substituted(self): + samba.check_all_substituted("nothing to see here") + self.assertRaises(Exception, samba.check_all_substituted, + "Not subsituted: ${FOOBAR}") + + +class LdbExtensionTests(TestCaseInTempDir): + + def test_searchone(self): + path = self.tempdir + "/searchone.ldb" + l = samba.Ldb(path) + try: + l.add({"dn": "foo=dc", "bar": "bla"}) + self.assertEquals("bla", + l.searchone(basedn=ldb.Dn(l, "foo=dc"), attribute="bar")) + finally: + del l + os.unlink(path) diff --git a/source4/scripting/python/samba/tests/credentials.py b/source4/scripting/python/samba/tests/credentials.py new file mode 100644 index 0000000000..5ed61c6b21 --- /dev/null +++ b/source4/scripting/python/samba/tests/credentials.py @@ -0,0 +1,100 @@ +#!/usr/bin/env 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/>. +# + +"""Tests for the Credentials Python bindings. + +Note that this just tests the bindings work. It does not intend to test +the functionality, that's already done in other tests. +""" + +from samba import credentials +import samba.tests + +class CredentialsTests(samba.tests.TestCase): + + def setUp(self): + super(CredentialsTests, self).setUp() + self.creds = credentials.Credentials() + + def test_set_username(self): + self.creds.set_username("somebody") + self.assertEquals("somebody", self.creds.get_username()) + + def test_set_password(self): + self.creds.set_password("S3CreT") + self.assertEquals("S3CreT", self.creds.get_password()) + + def test_set_domain(self): + self.creds.set_domain("ABMAS") + self.assertEquals("ABMAS", self.creds.get_domain()) + + def test_set_realm(self): + self.creds.set_realm("myrealm") + self.assertEquals("MYREALM", self.creds.get_realm()) + + def test_parse_string_anon(self): + self.creds.parse_string("%") + self.assertEquals("", self.creds.get_username()) + self.assertEquals(None, self.creds.get_password()) + + def test_parse_string_user_pw_domain(self): + self.creds.parse_string("dom\\someone%secr") + self.assertEquals("someone", self.creds.get_username()) + self.assertEquals("secr", self.creds.get_password()) + self.assertEquals("DOM", self.creds.get_domain()) + + def test_bind_dn(self): + self.assertEquals(None, self.creds.get_bind_dn()) + self.creds.set_bind_dn("dc=foo,cn=bar") + self.assertEquals("dc=foo,cn=bar", self.creds.get_bind_dn()) + + def test_is_anon(self): + self.creds.set_username("") + self.assertTrue(self.creds.is_anonymous()) + self.creds.set_username("somebody") + self.assertFalse(self.creds.is_anonymous()) + self.creds.set_anonymous() + self.assertTrue(self.creds.is_anonymous()) + + def test_workstation(self): + # FIXME: This is uninitialised, it should be None + #self.assertEquals(None, self.creds.get_workstation()) + self.creds.set_workstation("myworksta") + self.assertEquals("myworksta", self.creds.get_workstation()) + + def test_get_nt_hash(self): + self.creds.set_password("geheim") + self.assertEquals('\xc2\xae\x1f\xe6\xe6H\x84cRE>\x81o*\xeb\x93', + self.creds.get_nt_hash()) + + def test_guess(self): + # Just check the method is there and doesn't raise an exception + self.creds.guess() + + def test_set_cmdline_callbacks(self): + self.creds.set_cmdline_callbacks() + + def test_authentication_requested(self): + self.creds.set_username("") + self.assertFalse(self.creds.authentication_requested()) + self.creds.set_username("somebody") + self.assertTrue(self.creds.authentication_requested()) + + def test_wrong_password(self): + self.assertFalse(self.creds.wrong_password()) diff --git a/source4/scripting/python/samba/tests/dcerpc/__init__.py b/source4/scripting/python/samba/tests/dcerpc/__init__.py index c64c9dc9f6..0f9625570a 100644 --- a/source4/scripting/python/samba/tests/dcerpc/__init__.py +++ b/source4/scripting/python/samba/tests/dcerpc/__init__.py @@ -1,14 +1,14 @@ -#!/usr/bin/python +#!/usr/bin/env 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 @@ -16,5 +16,6 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# + +"""Tests for the DCE/RPC Python bindings.""" diff --git a/source4/scripting/python/samba/tests/dcerpc/bare.py b/source4/scripting/python/samba/tests/dcerpc/bare.py index 6cadad89f1..22163697f8 100644 --- a/source4/scripting/python/samba/tests/dcerpc/bare.py +++ b/source4/scripting/python/samba/tests/dcerpc/bare.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # -*- coding: utf-8 -*- # Unix SMB/CIFS implementation. @@ -18,32 +18,35 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # -from samba.dcerpc import ClientConnection -from unittest import TestCase -from samba.tests import cmdline_loadparm +"""Tests for samba.dcerpc.bare.""" +from samba.dcerpc import ClientConnection +import samba.tests -class BareTestCase(TestCase): +class BareTestCase(samba.tests.TestCase): def test_bare(self): # Connect to the echo pipe x = ClientConnection("ncalrpc:localhost[DEFAULT]", - ("60a15ec5-4de8-11d7-a637-005056a20182", 1), lp_ctx=cmdline_loadparm) + ("60a15ec5-4de8-11d7-a637-005056a20182", 1), + lp_ctx=samba.tests.env_loadparm()) self.assertEquals("\x01\x00\x00\x00", x.request(0, chr(0) * 4)) def test_alter_context(self): x = ClientConnection("ncalrpc:localhost[DEFAULT]", - ("12345778-1234-abcd-ef00-0123456789ac", 1), lp_ctx=cmdline_loadparm) + ("12345778-1234-abcd-ef00-0123456789ac", 1), + lp_ctx=samba.tests.env_loadparm()) y = ClientConnection("ncalrpc:localhost", ("60a15ec5-4de8-11d7-a637-005056a20182", 1), - basis_connection=x, lp_ctx=cmdline_loadparm) + basis_connection=x, lp_ctx=samba.tests.env_loadparm()) x.alter_context(("60a15ec5-4de8-11d7-a637-005056a20182", 1)) # FIXME: self.assertEquals("\x01\x00\x00\x00", x.request(0, chr(0) * 4)) def test_two_connections(self): x = ClientConnection("ncalrpc:localhost[DEFAULT]", - ("60a15ec5-4de8-11d7-a637-005056a20182", 1), lp_ctx=cmdline_loadparm) + ("60a15ec5-4de8-11d7-a637-005056a20182", 1), + lp_ctx=samba.tests.env_loadparm()) y = ClientConnection("ncalrpc:localhost", ("60a15ec5-4de8-11d7-a637-005056a20182", 1), - basis_connection=x, lp_ctx=cmdline_loadparm) + basis_connection=x, lp_ctx=samba.tests.env_loadparm()) self.assertEquals("\x01\x00\x00\x00", y.request(0, chr(0) * 4)) diff --git a/source4/scripting/python/samba/tests/dcerpc/misc.py b/source4/scripting/python/samba/tests/dcerpc/misc.py index ab76efc536..37a647a842 100644 --- a/source4/scripting/python/samba/tests/dcerpc/misc.py +++ b/source4/scripting/python/samba/tests/dcerpc/misc.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # Unix SMB/CIFS implementation. # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007 @@ -17,13 +17,15 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # -import unittest +"""Tests for samba.dcerpc.misc.""" + from samba.dcerpc import misc +import samba.tests text1 = "76f53846-a7c2-476a-ae2c-20e2b80d7b34" text2 = "344edffa-330a-4b39-b96e-2c34da52e8b1" -class GUIDTests(unittest.TestCase): +class GUIDTests(samba.tests.TestCase): def test_str(self): guid = misc.GUID(text1) @@ -43,9 +45,9 @@ class GUIDTests(unittest.TestCase): guid2 = misc.GUID(text1) self.assertEquals(0, cmp(guid1, guid2)) self.assertEquals(guid1, guid2) - - -class PolicyHandleTests(unittest.TestCase): + + +class PolicyHandleTests(samba.tests.TestCase): def test_init(self): x = misc.policy_handle(text1, 1) diff --git a/source4/scripting/python/samba/tests/dcerpc/registry.py b/source4/scripting/python/samba/tests/dcerpc/registry.py index f1cd83790d..f0716d5ffb 100644 --- a/source4/scripting/python/samba/tests/dcerpc/registry.py +++ b/source4/scripting/python/samba/tests/dcerpc/registry.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # Unix SMB/CIFS implementation. # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2008 @@ -17,6 +17,8 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # +"""Tests for samba.dcerpc.registry.""" + from samba.dcerpc import winreg from samba.tests import RpcInterfaceTestCase @@ -24,6 +26,7 @@ from samba.tests import RpcInterfaceTestCase class WinregTests(RpcInterfaceTestCase): def setUp(self): + super(WinregTests, self).setUp() self.conn = winreg.winreg("ncalrpc:", self.get_loadparm(), self.get_credentials()) diff --git a/source4/scripting/python/samba/tests/dcerpc/rpc_talloc.py b/source4/scripting/python/samba/tests/dcerpc/rpc_talloc.py new file mode 100755 index 0000000000..fe0f93ef9b --- /dev/null +++ b/source4/scripting/python/samba/tests/dcerpc/rpc_talloc.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +# test generated python code from pidl +# Andrew Tridgell August 2010 +# +# to run this test, use one of these: +# +# python -m testtools.run samba.tests.dcerpc.rpc_talloc +# +# or if you have trial installed (from twisted), use +# +# trial samba.tests.dcerpc.rpc_talloc + +"""Tests for the talloc handling in the generated Python DCE/RPC bindings.""" + +import sys + +sys.path.insert(0, "bin/python") + +import samba +import samba.tests +from samba.dcerpc import drsuapi +import talloc + +talloc.enable_null_tracking() + +class TallocTests(samba.tests.TestCase): + '''test talloc behaviour of pidl generated python code''' + + def check_blocks(self, object, num_expected): + '''check that the number of allocated blocks is correct''' + nblocks = talloc.total_blocks(object) + if object is None: + nblocks -= self.initial_blocks + self.assertEquals(nblocks, num_expected) + + def get_rodc_partial_attribute_set(self): + '''get a list of attributes for RODC replication''' + partial_attribute_set = drsuapi.DsPartialAttributeSet() + + # we expect one block for the object, and one for the structure + self.check_blocks(partial_attribute_set, 2) + + attids = [ 1, 2, 3] + partial_attribute_set.version = 1 + partial_attribute_set.attids = attids + partial_attribute_set.num_attids = len(attids) + + # we expect one block object, a structure, an ARRAY, and a + # reference to the array + self.check_blocks(partial_attribute_set, 3) + + return partial_attribute_set + + def pas_test(self): + pas = self.get_rodc_partial_attribute_set() + self.check_blocks(pas, 3) + req8 = drsuapi.DsGetNCChangesRequest8() + self.check_blocks(req8, 2) + self.check_blocks(None, 5) + req8.partial_attribute_set = pas + if req8.partial_attribute_set.attids[1] != 2: + raise Exception("Wrong value in attids[2]") + # we now get an additional reference + self.check_blocks(None, 6) + + def test_run(self): + self.initial_blocks = talloc.total_blocks(None) + self.check_blocks(None, 0) + self.pas_test() + self.check_blocks(None, 0) diff --git a/source4/scripting/python/samba/tests/dcerpc/rpcecho.py b/source4/scripting/python/samba/tests/dcerpc/rpcecho.py index 72eb87d750..ba7ad9688b 100644 --- a/source4/scripting/python/samba/tests/dcerpc/rpcecho.py +++ b/source4/scripting/python/samba/tests/dcerpc/rpcecho.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # Unix SMB/CIFS implementation. # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2008 @@ -17,15 +17,17 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # +"""Tests for samba.dceprc.rpcecho.""" + from samba.dcerpc import echo from samba.ndr import ndr_pack, ndr_unpack -import unittest -from samba.tests import RpcInterfaceTestCase +from samba.tests import RpcInterfaceTestCase, TestCase class RpcEchoTests(RpcInterfaceTestCase): def setUp(self): + super(RpcEchoTests, self).setUp() self.conn = echo.rpcecho("ncalrpc:", self.get_loadparm()) def test_two_contexts(self): @@ -33,7 +35,7 @@ class RpcEchoTests(RpcInterfaceTestCase): self.assertEquals(3, self.conn2.AddOne(2)) def test_abstract_syntax(self): - self.assertEquals(("60a15ec5-4de8-11d7-a637-005056a20182", 1), + self.assertEquals(("60a15ec5-4de8-11d7-a637-005056a20182", 1), self.conn.abstract_syntax) def test_addone(self): @@ -59,7 +61,7 @@ class RpcEchoTests(RpcInterfaceTestCase): self.assertEquals(None, self.conn.server_name) -class NdrEchoTests(unittest.TestCase): +class NdrEchoTests(TestCase): def test_info1_push(self): x = echo.info1() diff --git a/source4/scripting/python/samba/tests/dcerpc/sam.py b/source4/scripting/python/samba/tests/dcerpc/sam.py index 9532333a00..38b381d3aa 100644 --- a/source4/scripting/python/samba/tests/dcerpc/sam.py +++ b/source4/scripting/python/samba/tests/dcerpc/sam.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # -*- coding: utf-8 -*- # Unix SMB/CIFS implementation. @@ -18,6 +18,8 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # +"""Tests for samba.dcerpc.sam.""" + from samba.dcerpc import samr, security from samba.tests import RpcInterfaceTestCase @@ -32,6 +34,7 @@ def toArray((handle, array, num_entries)): class SamrTests(RpcInterfaceTestCase): def setUp(self): + super(SamrTests, self).setUp() self.conn = samr.samr("ncalrpc:", self.get_loadparm()) def test_connect5(self): @@ -39,6 +42,7 @@ class SamrTests(RpcInterfaceTestCase): def test_connect2(self): handle = self.conn.Connect2(None, security.SEC_FLAG_MAXIMUM_ALLOWED) + self.assertTrue(handle is not None) def test_EnumDomains(self): handle = self.conn.Connect2(None, security.SEC_FLAG_MAXIMUM_ALLOWED) diff --git a/source4/scripting/python/samba/tests/dcerpc/testrpc.py b/source4/scripting/python/samba/tests/dcerpc/testrpc.py new file mode 100644 index 0000000000..2f3a6a0aef --- /dev/null +++ b/source4/scripting/python/samba/tests/dcerpc/testrpc.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python +# +# test generated python code from pidl +# Andrew Tridgell August 2010 +# +import sys + +sys.path.insert(0, "bin/python") + +import samba +import samba.tests +from samba.dcerpc import drsuapi +import talloc + +talloc.enable_null_tracking() + +class RpcTests(object): + '''test type behaviour of pidl generated python RPC code''' + + def check_blocks(self, object, num_expected): + '''check that the number of allocated blocks is correct''' + nblocks = talloc.total_blocks(object) + if object is None: + nblocks -= self.initial_blocks + leaked_blocks = (nblocks - num_expected) + if leaked_blocks != 0: + print "Leaked %d blocks" % leaked_blocks + + def check_type(self, interface, typename, type): + print "Checking type %s" % typename + v = type() + for n in dir(v): + if n[0] == '_': + continue + try: + value = getattr(v, n) + except TypeError, errstr: + if str(errstr) == "unknown union level": + print "ERROR: Unknown union level in %s.%s" % (typename, n) + self.errcount += 1 + continue + print str(errstr)[1:21] + if str(errstr)[0:21] == "Can not convert C Type": + print "ERROR: Unknown C type for %s.%s" % (typename, n) + self.errcount += 1 + continue + else: + print "ERROR: Failed to instantiate %s.%s" % (typename, n) + self.errcount += 1 + continue + except: + print "ERROR: Failed to instantiate %s.%s" % (typename, n) + self.errcount += 1 + continue + + # now try setting the value back + try: + print "Setting %s.%s" % (typename, n) + setattr(v, n, value) + except Exception, e: + if isinstance(e, AttributeError) and str(e).endswith("is read-only"): + # readonly, ignore + continue + else: + print "ERROR: Failed to set %s.%s: %r: %s" % (typename, n, e.__class__, e) + self.errcount += 1 + continue + + # and try a comparison + try: + if value != getattr(v, n): + print "ERROR: Comparison failed for %s.%s: %r != %r" % (typename, n, value, getattr(v, n)) + continue + except Exception, e: + print "ERROR: compare exception for %s.%s: %r: %s" % (typename, n, e.__class__, e) + continue + + def check_interface(self, interface, iname): + errcount = self.errcount + for n in dir(interface): + if n[0] == '_' or n == iname: + # skip the special ones + continue + value = getattr(interface, n) + if isinstance(value, str): + #print "%s=\"%s\"" % (n, value) + pass + elif isinstance(value, int) or isinstance(value, long): + #print "%s=%d" % (n, value) + pass + elif isinstance(value, type): + try: + initial_blocks = talloc.total_blocks(None) + self.check_type(interface, n, value) + self.check_blocks(None, initial_blocks) + except Exception, e: + print "ERROR: Failed to check_type %s.%s: %r: %s" % (iname, n, e.__class__, e) + self.errcount += 1 + elif callable(value): + pass # Method + else: + print "UNKNOWN: %s=%s" % (n, value) + if self.errcount - errcount != 0: + print "Found %d errors in %s" % (self.errcount - errcount, iname) + + def check_all_interfaces(self): + for iname in dir(samba.dcerpc): + if iname[0] == '_': + continue + if iname == 'ClientConnection' or iname == 'base': + continue + print "Checking interface %s" % iname + iface = getattr(samba.dcerpc, iname) + initial_blocks = talloc.total_blocks(None) + self.check_interface(iface, iname) + self.check_blocks(None, initial_blocks) + + def run(self): + self.initial_blocks = talloc.total_blocks(None) + self.errcount = 0 + self.check_all_interfaces() + return self.errcount + +tests = RpcTests() +errcount = tests.run() +if errcount == 0: + sys.exit(0) +else: + print "%d failures" % errcount + sys.exit(1) diff --git a/source4/scripting/python/samba/tests/dcerpc/unix.py b/source4/scripting/python/samba/tests/dcerpc/unix.py index 62169ad12c..16bf37e749 100644 --- a/source4/scripting/python/samba/tests/dcerpc/unix.py +++ b/source4/scripting/python/samba/tests/dcerpc/unix.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # Unix SMB/CIFS implementation. # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2008 @@ -17,22 +17,35 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # +"""Tests for samba.dcerpc.unixinfo.""" + + from samba.dcerpc import unixinfo from samba.tests import RpcInterfaceTestCase class UnixinfoTests(RpcInterfaceTestCase): def setUp(self): + super(UnixinfoTests, self).setUp() self.conn = unixinfo.unixinfo("ncalrpc:", self.get_loadparm()) - def test_getpwuid(self): + def test_getpwuid_int(self): infos = self.conn.GetPWUid(range(512)) self.assertEquals(512, len(infos)) self.assertEquals("/bin/false", infos[0].shell) self.assertTrue(isinstance(infos[0].homedir, unicode)) + def test_getpwuid(self): + infos = self.conn.GetPWUid(map(long, range(512))) + self.assertEquals(512, len(infos)) + self.assertEquals("/bin/false", infos[0].shell) + self.assertTrue(isinstance(infos[0].homedir, unicode)) + def test_gidtosid(self): - self.conn.GidToSid(1000) + self.conn.GidToSid(1000L) def test_uidtosid(self): self.conn.UidToSid(1000) + + def test_uidtosid_fail(self): + self.assertRaises(TypeError, self.conn.UidToSid, "100") diff --git a/source4/scripting/python/samba/tests/dsdb.py b/source4/scripting/python/samba/tests/dsdb.py new file mode 100644 index 0000000000..d4331f3ef5 --- /dev/null +++ b/source4/scripting/python/samba/tests/dsdb.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python + +# Unix SMB/CIFS implementation. Tests for dsdb +# Copyright (C) Matthieu Patou <mat@matws.net> 2010 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +"""Tests for samba.dsdb.""" + +from samba.credentials import Credentials +from samba.samdb import SamDB +from samba.auth import system_session +from testtools.testcase import TestCase +from samba.ndr import ndr_unpack, ndr_pack +from samba.dcerpc import drsblobs +import ldb +import os +import samba + + +class DsdbTests(TestCase): + + + def setUp(self): + super(DsdbTests, self).setUp() + self.lp = samba.param.LoadParm() + self.lp.load(os.path.join(os.path.join(self.baseprovpath(), "etc"), "smb.conf")) + self.creds = Credentials() + self.creds.guess(self.lp) + self.session = system_session() + self.samdb = SamDB(os.path.join(self.baseprovpath(), "private", "sam.ldb"), + session_info=self.session, credentials=self.creds,lp=self.lp) + + + def baseprovpath(self): + return os.path.join(os.environ['SELFTEST_PREFIX'], "dc") + + + def test_get_oid_from_attrid(self): + oid = self.samdb.get_oid_from_attid(591614) + self.assertEquals(oid, "1.2.840.113556.1.4.1790") + + def test_error_replpropertymetadata(self): + res = self.samdb.search(expression="cn=Administrator", + scope=ldb.SCOPE_SUBTREE, + attrs=["replPropertyMetaData"]) + repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob, + str(res[0]["replPropertyMetaData"])) + ctr = repl.ctr + for o in ctr.array: + # Search for Description + if o.attid == 13: + old_version = o.version + o.version = o.version + 1 + replBlob = ndr_pack(repl) + msg = ldb.Message() + msg.dn = res[0].dn + msg["replPropertyMetaData"] = ldb.MessageElement(replBlob, ldb.FLAG_MOD_REPLACE, "replPropertyMetaData") + self.assertRaises(ldb.LdbError, self.samdb.modify, msg, ["local_oid:1.3.6.1.4.1.7165.4.3.14:0"]) + + def test_twoatt_replpropertymetadata(self): + res = self.samdb.search(expression="cn=Administrator", + scope=ldb.SCOPE_SUBTREE, + attrs=["replPropertyMetaData", "uSNChanged"]) + repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob, + str(res[0]["replPropertyMetaData"])) + ctr = repl.ctr + for o in ctr.array: + # Search for Description + if o.attid == 13: + old_version = o.version + o.version = o.version + 1 + o.local_usn = long(str(res[0]["uSNChanged"])) + 1 + replBlob = ndr_pack(repl) + msg = ldb.Message() + msg.dn = res[0].dn + msg["replPropertyMetaData"] = ldb.MessageElement(replBlob, ldb.FLAG_MOD_REPLACE, "replPropertyMetaData") + msg["description"] = ldb.MessageElement("new val", ldb.FLAG_MOD_REPLACE, "description") + self.assertRaises(ldb.LdbError, self.samdb.modify, msg, ["local_oid:1.3.6.1.4.1.7165.4.3.14:0"]) + + def test_set_replpropertymetadata(self): + res = self.samdb.search(expression="cn=Administrator", + scope=ldb.SCOPE_SUBTREE, + attrs=["replPropertyMetaData", "uSNChanged"]) + repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob, + str(res[0]["replPropertyMetaData"])) + ctr = repl.ctr + for o in ctr.array: + # Search for Description + if o.attid == 13: + old_version = o.version + o.version = o.version + 1 + o.local_usn = long(str(res[0]["uSNChanged"])) + 1 + o.originating_usn = long(str(res[0]["uSNChanged"])) + 1 + replBlob = ndr_pack(repl) + msg = ldb.Message() + msg.dn = res[0].dn + msg["replPropertyMetaData"] = ldb.MessageElement(replBlob, ldb.FLAG_MOD_REPLACE, "replPropertyMetaData") + self.samdb.modify(msg, ["local_oid:1.3.6.1.4.1.7165.4.3.14:0"]) + + def test_ok_get_attribute_from_attid(self): + self.assertEquals(self.samdb.get_attribute_from_attid(13), "description") + + def test_ko_get_attribute_from_attid(self): + self.assertEquals(self.samdb.get_attribute_from_attid(11979), None) + + def test_get_attribute_replmetadata_version(self): + res = self.samdb.search(expression="cn=Administrator", + scope=ldb.SCOPE_SUBTREE, + attrs=["dn"]) + self.assertEquals(len(res), 1) + dn = str(res[0].dn) + self.assertEqual(self.samdb.get_attribute_replmetadata_version(dn, "unicodePwd"), 1) + + def test_set_attribute_replmetadata_version(self): + res = self.samdb.search(expression="cn=Administrator", + scope=ldb.SCOPE_SUBTREE, + attrs=["dn"]) + self.assertEquals(len(res), 1) + dn = str(res[0].dn) + version = self.samdb.get_attribute_replmetadata_version(dn, "description") + self.samdb.set_attribute_replmetadata_version(dn, "description", version + 2) + self.assertEqual(self.samdb.get_attribute_replmetadata_version(dn, "description"), version + 2) diff --git a/source4/scripting/python/samba/tests/gensec.py b/source4/scripting/python/samba/tests/gensec.py new file mode 100644 index 0000000000..ddca0df980 --- /dev/null +++ b/source4/scripting/python/samba/tests/gensec.py @@ -0,0 +1,90 @@ +#!/usr/bin/env 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/>. +# + +"""Tests for GENSEC. + +Note that this just tests the bindings work. It does not intend to test +the functionality, that's already done in other tests. +""" + +from samba.credentials import Credentials +from samba import gensec +import samba.tests + +class GensecTests(samba.tests.TestCase): + + def setUp(self): + super(GensecTests, self).setUp() + self.settings = {} + self.settings["lp_ctx"] = self.lp_ctx = samba.tests.env_loadparm() + self.settings["target_hostname"] = self.lp_ctx.get("netbios name") + """This is just for the API tests""" + self.gensec = gensec.Security.start_client(self.settings) + + def test_start_mech_by_unknown_name(self): + self.assertRaises(RuntimeError, self.gensec.start_mech_by_name, "foo") + + def test_start_mech_by_name(self): + self.gensec.start_mech_by_name("spnego") + + def test_info_uninitialized(self): + self.assertRaises(RuntimeError, self.gensec.session_info) + + def test_update(self): + """Test GENSEC by doing an exchange with ourselves using GSSAPI against a KDC""" + + """Start up a client and server GENSEC instance to test things with""" + + self.gensec_client = gensec.Security.start_client(self.settings) + self.gensec_client.set_credentials(self.get_credentials()) + self.gensec_client.want_feature(gensec.FEATURE_SEAL) + self.gensec_client.start_mech_by_sasl_name("GSSAPI") + + self.gensec_server = gensec.Security.start_server(self.settings) + creds = Credentials() + creds.guess(self.lp_ctx) + creds.set_machine_account(self.lp_ctx) + self.gensec_server.set_credentials(creds) + + self.gensec_server.want_feature(gensec.FEATURE_SEAL) + self.gensec_server.start_mech_by_sasl_name("GSSAPI") + + client_finished = False + server_finished = False + server_to_client = "" + + """Run the actual call loop""" + while client_finished == False and server_finished == False: + if not client_finished: + print "running client gensec_update" + (client_finished, client_to_server) = self.gensec_client.update(server_to_client) + if not server_finished: + print "running server gensec_update" + (server_finished, server_to_client) = self.gensec_server.update(client_to_server) + session_info = self.gensec_server.session_info() + + test_string = "Hello Server" + test_wrapped = self.gensec_client.wrap(test_string) + test_unwrapped = self.gensec_server.unwrap(test_wrapped) + self.assertEqual(test_string, test_unwrapped) + test_string = "Hello Client" + test_wrapped = self.gensec_server.wrap(test_string) + test_unwrapped = self.gensec_client.unwrap(test_wrapped) + self.assertEqual(test_string, test_unwrapped) + diff --git a/source4/scripting/python/samba/tests/shares.py b/source4/scripting/python/samba/tests/hostconfig.py index 9130c36780..78ca6202b2 100644 --- a/source4/scripting/python/samba/tests/shares.py +++ b/source4/scripting/python/samba/tests/hostconfig.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # Unix SMB/CIFS implementation. Tests for shares # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2009 @@ -16,8 +16,11 @@ # 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 + +"""Tests for samba.hostconfig.""" + +from samba.hostconfig import SharesContainer +from samba.tests import TestCase class MockService(object): @@ -37,9 +40,6 @@ class MockLoadParm(object): 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) diff --git a/source4/scripting/python/samba/tests/messaging.py b/source4/scripting/python/samba/tests/messaging.py new file mode 100644 index 0000000000..d2a0b73775 --- /dev/null +++ b/source4/scripting/python/samba/tests/messaging.py @@ -0,0 +1,60 @@ +#!/usr/bin/env 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/>. +# + +"""Tests for samba.messaging.""" + +from samba.messaging import Messaging +from samba.tests import TestCase + +class MessagingTests(TestCase): + + def get_context(self, *args, **kwargs): + kwargs["messaging_path"] = "." + return Messaging(*args, **kwargs) + + def test_register(self): + x = self.get_context() + def callback(): + pass + msg_type = x.register(callback) + x.deregister(callback, msg_type) + + def test_assign_server_id(self): + x = self.get_context() + self.assertTrue(isinstance(x.server_id, tuple)) + self.assertEquals(3, len(x.server_id)) + + def test_ping_speed(self): + server_ctx = self.get_context((0, 1)) + def ping_callback(src, data): + server_ctx.send(src, data) + def exit_callback(): + print "received exit" + msg_ping = server_ctx.register(ping_callback) + msg_exit = server_ctx.register(exit_callback) + + def pong_callback(): + print "received pong" + client_ctx = self.get_context((0, 2)) + msg_pong = client_ctx.register(pong_callback) + + client_ctx.send((0, 1), msg_ping, "testing") + client_ctx.send((0, 1), msg_ping, "") + diff --git a/source4/scripting/python/samba/tests/netcmd.py b/source4/scripting/python/samba/tests/netcmd.py new file mode 100644 index 0000000000..787bcd5a72 --- /dev/null +++ b/source4/scripting/python/samba/tests/netcmd.py @@ -0,0 +1,35 @@ +#!/usr/bin/env 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/>. +# + +"""Tests for samba.netcmd.""" + +from samba.netcmd import Command +import samba.tests + +class CommandTests(samba.tests.TestCase): + + def test_name(self): + class cmd_foo(Command): + pass + self.assertEquals("foo", cmd_foo().name) + + def test_description(self): + class cmd_foo(Command): + """Mydescription""" + self.assertEquals("Mydescription", cmd_foo().description) diff --git a/source4/scripting/python/samba/tests/ntacls.py b/source4/scripting/python/samba/tests/ntacls.py new file mode 100644 index 0000000000..2d8d6b9d67 --- /dev/null +++ b/source4/scripting/python/samba/tests/ntacls.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python + +# Unix SMB/CIFS implementation. Tests for ntacls manipulation +# Copyright (C) Matthieu Patou <mat@matws.net> 2009-2010 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +"""Tests for samba.ntacls.""" + +from samba.ntacls import setntacl, getntacl, XattrBackendError +from samba.dcerpc import xattr, security +from samba.param import LoadParm +from samba.tests import TestCase, TestSkipped +import random +import os + +class NtaclsTests(TestCase): + + def test_setntacl(self): + random.seed() + lp = LoadParm() + path = os.environ['SELFTEST_PREFIX'] + acl = "O:S-1-5-21-2212615479-2695158682-2101375467-512G:S-1-5-21-2212615479-2695158682-2101375467-513D:(A;OICI;0x001f01ff;;;S-1-5-21-2212615479-2695158682-2101375467-512)" + tempf = os.path.join(path,"pytests"+str(int(100000*random.random()))) + ntacl = xattr.NTACL() + ntacl.version = 1 + open(tempf, 'w').write("empty") + lp.set("posix:eadb",os.path.join(path,"eadbtest.tdb")) + setntacl(lp, tempf, acl, "S-1-5-21-2212615479-2695158682-2101375467") + os.unlink(tempf) + + def test_setntacl_getntacl(self): + random.seed() + lp = LoadParm() + path = None + path = os.environ['SELFTEST_PREFIX'] + acl = "O:S-1-5-21-2212615479-2695158682-2101375467-512G:S-1-5-21-2212615479-2695158682-2101375467-513D:(A;OICI;0x001f01ff;;;S-1-5-21-2212615479-2695158682-2101375467-512)" + tempf = os.path.join(path,"pytests"+str(int(100000*random.random()))) + ntacl = xattr.NTACL() + ntacl.version = 1 + open(tempf, 'w').write("empty") + lp.set("posix:eadb",os.path.join(path,"eadbtest.tdb")) + setntacl(lp,tempf,acl,"S-1-5-21-2212615479-2695158682-2101375467") + facl = getntacl(lp,tempf) + anysid = security.dom_sid(security.SID_NT_SELF) + self.assertEquals(facl.info.as_sddl(anysid),acl) + os.unlink(tempf) + + def test_setntacl_getntacl_param(self): + random.seed() + lp = LoadParm() + acl = "O:S-1-5-21-2212615479-2695158682-2101375467-512G:S-1-5-21-2212615479-2695158682-2101375467-513D:(A;OICI;0x001f01ff;;;S-1-5-21-2212615479-2695158682-2101375467-512)" + path = os.environ['SELFTEST_PREFIX'] + tempf = os.path.join(path,"pytests"+str(int(100000*random.random()))) + ntacl = xattr.NTACL() + ntacl.version = 1 + open(tempf, 'w').write("empty") + setntacl(lp,tempf,acl,"S-1-5-21-2212615479-2695158682-2101375467","tdb",os.path.join(path,"eadbtest.tdb")) + facl=getntacl(lp,tempf,"tdb",os.path.join(path,"eadbtest.tdb")) + domsid=security.dom_sid(security.SID_NT_SELF) + self.assertEquals(facl.info.as_sddl(domsid),acl) + os.unlink(tempf) + + def test_setntacl_invalidbackend(self): + random.seed() + lp = LoadParm() + acl = "O:S-1-5-21-2212615479-2695158682-2101375467-512G:S-1-5-21-2212615479-2695158682-2101375467-513D:(A;OICI;0x001f01ff;;;S-1-5-21-2212615479-2695158682-2101375467-512)" + path = os.environ['SELFTEST_PREFIX'] + tempf = os.path.join(path,"pytests"+str(int(100000*random.random()))) + ntacl = xattr.NTACL() + ntacl.version = 1 + open(tempf, 'w').write("empty") + self.assertRaises(XattrBackendError, setntacl, lp, tempf, acl, "S-1-5-21-2212615479-2695158682-2101375467","ttdb", os.path.join(path,"eadbtest.tdb")) + + def test_setntacl_forcenative(self): + if os.getuid() == 0: + raise TestSkipped("Running test as root, test skipped") + random.seed() + lp = LoadParm() + acl = "O:S-1-5-21-2212615479-2695158682-2101375467-512G:S-1-5-21-2212615479-2695158682-2101375467-513D:(A;OICI;0x001f01ff;;;S-1-5-21-2212615479-2695158682-2101375467-512)" + path = os.environ['SELFTEST_PREFIX'] + tempf = os.path.join(path,"pytests"+str(int(100000*random.random()))) + ntacl = xattr.NTACL() + ntacl.version = 1 + open(tempf, 'w').write("empty") + lp.set("posix:eadb", os.path.join(path,"eadbtest.tdb")) + self.assertRaises(Exception, setntacl, lp, tempf ,acl, + "S-1-5-21-2212615479-2695158682-2101375467","native") + os.unlink(tempf) diff --git a/source4/scripting/python/samba/tests/param.py b/source4/scripting/python/samba/tests/param.py new file mode 100644 index 0000000000..7848e1c23b --- /dev/null +++ b/source4/scripting/python/samba/tests/param.py @@ -0,0 +1,59 @@ +#!/usr/bin/env 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/>. +# + +"""Tests for samba.param.""" + +from samba import param +import samba.tests + +class LoadParmTestCase(samba.tests.TestCase): + + def test_init(self): + file = param.LoadParm() + self.assertTrue(file is not None) + + def test_length(self): + file = param.LoadParm() + self.assertEquals(0, len(file)) + + def test_set_workgroup(self): + file = param.LoadParm() + file.set("workgroup", "bla") + self.assertEquals("BLA", file.get("workgroup")) + + def test_is_mydomain(self): + file = param.LoadParm() + file.set("workgroup", "bla") + self.assertTrue(file.is_mydomain("BLA")) + self.assertFalse(file.is_mydomain("FOOBAR")) + + def test_is_myname(self): + file = param.LoadParm() + file.set("netbios name", "bla") + self.assertTrue(file.is_myname("BLA")) + self.assertFalse(file.is_myname("FOOBAR")) + + def test_load_default(self): + file = param.LoadParm() + file.load_default() + + def test_section_nonexistant(self): + samba_lp = param.LoadParm() + samba_lp.load_default() + self.assertRaises(KeyError, samba_lp.__getitem__, "nonexistant") diff --git a/source4/scripting/python/samba/tests/provision.py b/source4/scripting/python/samba/tests/provision.py index f34073504c..39a01606bb 100644 --- a/source4/scripting/python/samba/tests/provision.py +++ b/source4/scripting/python/samba/tests/provision.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # Unix SMB/CIFS implementation. # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2008 @@ -17,26 +17,44 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # +"""Tests for samba.provision.""" + import os -from samba.provision import setup_secretsdb, findnss +from samba.provision import setup_secretsdb, findnss, ProvisionPaths import samba.tests -from ldb import Dn -from samba import param -import unittest +from samba.tests import env_loadparm, TestCase -lp = samba.tests.cmdline_loadparm +def create_dummy_secretsdb(path, lp=None): + """Create a dummy secrets database for use in tests. -setup_dir = "setup" -def setup_path(file): - return os.path.join(setup_dir, file) + :param path: Path to store the secrets db + :param lp: Optional loadparm context. A simple one will + be generated if not specified. + """ + if lp is None: + lp = env_loadparm() + paths = ProvisionPaths() + paths.secrets = path + paths.private_dir = os.path.dirname(path) + paths.keytab = "no.keytab" + paths.dns_keytab = "no.dns.keytab" + secrets_ldb = setup_secretsdb(paths, None, None, lp=lp) + secrets_ldb.transaction_commit() + return secrets_ldb class ProvisionTestCase(samba.tests.TestCaseInTempDir): """Some simple tests for individual functions in the provisioning code. """ + def test_setup_secretsdb(self): path = os.path.join(self.tempdir, "secrets.ldb") - ldb = setup_secretsdb(path, setup_path, None, None, lp=lp) + paths = ProvisionPaths() + paths.secrets = path + paths.private_dir = os.path.dirname(path) + paths.keytab = "no.keytab" + paths.dns_keytab = "no.dns.keytab" + ldb = setup_secretsdb(paths, None, None, lp=env_loadparm()) try: self.assertEquals("LSA Secrets", ldb.searchone(basedn="CN=LSA Secrets", attribute="CN")) @@ -45,8 +63,9 @@ class ProvisionTestCase(samba.tests.TestCaseInTempDir): os.unlink(path) -class FindNssTests(unittest.TestCase): +class FindNssTests(TestCase): """Test findnss() function.""" + def test_nothing(self): def x(y): raise KeyError @@ -64,6 +83,7 @@ class FindNssTests(unittest.TestCase): class Disabled(object): + def test_setup_templatesdb(self): raise NotImplementedError(self.test_setup_templatesdb) @@ -94,7 +114,4 @@ class Disabled(object): def test_vampire(self): raise NotImplementedError(self.test_vampire) - def test_erase_partitions(self): - raise NotImplementedError(self.test_erase_partitions) - diff --git a/source4/scripting/python/samba/tests/registry.py b/source4/scripting/python/samba/tests/registry.py new file mode 100644 index 0000000000..97926850d2 --- /dev/null +++ b/source4/scripting/python/samba/tests/registry.py @@ -0,0 +1,62 @@ +#!/usr/bin/env 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/>. +# + +"""Tests for samba.registry.""" + +import os +from samba import registry +import samba.tests + +class HelperTests(samba.tests.TestCase): + + def test_predef_to_name(self): + self.assertEquals("HKEY_LOCAL_MACHINE", + registry.get_predef_name(0x80000002)) + + def test_str_regtype(self): + self.assertEquals("REG_DWORD", registry.str_regtype(4)) + + + +class HiveTests(samba.tests.TestCaseInTempDir): + + def setUp(self): + super(HiveTests, self).setUp() + self.hive_path = os.path.join(self.tempdir, "ldb_new.ldb") + self.hive = registry.open_ldb(self.hive_path) + + def tearDown(self): + del self.hive + os.unlink(self.hive_path) + super(HiveTests, self).tearDown() + + def test_ldb_new(self): + self.assertTrue(self.hive is not None) + + #def test_flush(self): + # self.hive.flush() + + #def test_del_value(self): + # self.hive.del_value("FOO") + + +class RegistryTests(samba.tests.TestCase): + + def test_new(self): + self.registry = registry.Registry() diff --git a/source4/scripting/python/samba/tests/samba3.py b/source4/scripting/python/samba/tests/samba3.py index 71e08bdd7f..3a4b851c75 100644 --- a/source4/scripting/python/samba/tests/samba3.py +++ b/source4/scripting/python/samba/tests/samba3.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # Unix SMB/CIFS implementation. # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007 @@ -17,20 +17,30 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # -import unittest -from samba.samba3 import GroupMappingDatabase, Registry, PolicyDatabase, SecretsDatabase, TdbSam -from samba.samba3 import WinsDatabase, SmbpasswdFile, ACB_NORMAL, IdmapDatabase, SAMUser, ParamFile +"""Tests for samba.samba3.""" + +from samba.samba3 import (GroupMappingDatabase, Registry, PolicyDatabase, + SecretsDatabase, TdbSam) +from samba.samba3 import (WinsDatabase, SmbpasswdFile, ACB_NORMAL, + IdmapDatabase, SAMUser, ParamFile) +from samba.tests import TestCase import os -DATADIR=os.path.join(os.path.dirname(__file__), "../../../../../testdata/samba3") -print "Samba 3 data dir: %s" % DATADIR +for p in [ "../../../../../testdata/samba3", "../../../../testdata/samba3" ]: + DATADIR = os.path.join(os.path.dirname(__file__), p) + if os.path.exists(DATADIR): + break + + +class RegistryTestCase(TestCase): -class RegistryTestCase(unittest.TestCase): def setUp(self): + super(RegistryTestCase, self).setUp() self.registry = Registry(os.path.join(DATADIR, "registry.tdb")) def tearDown(self): self.registry.close() + super(RegistryTestCase, self).tearDown() def test_length(self): self.assertEquals(28, len(self.registry)) @@ -47,8 +57,10 @@ class RegistryTestCase(unittest.TestCase): self.registry.values("HKLM/SYSTEM/CURRENTCONTROLSET/SERVICES/EVENTLOG")) -class PolicyTestCase(unittest.TestCase): +class PolicyTestCase(TestCase): + def setUp(self): + super(PolicyTestCase, self).setUp() self.policy = PolicyDatabase(os.path.join(DATADIR, "account_policy.tdb")) def test_policy(self): @@ -64,12 +76,15 @@ class PolicyTestCase(unittest.TestCase): self.assertEquals(self.policy.bad_lockout_minutes, None) -class GroupsTestCase(unittest.TestCase): +class GroupsTestCase(TestCase): + def setUp(self): + super(GroupsTestCase, self).setUp() self.groupdb = GroupMappingDatabase(os.path.join(DATADIR, "group_mapping.tdb")) def tearDown(self): self.groupdb.close() + super(GroupsTestCase, self).tearDown() def test_group_length(self): self.assertEquals(13, len(list(self.groupdb.groupsids()))) @@ -85,23 +100,29 @@ class GroupsTestCase(unittest.TestCase): self.assertEquals(0, len(list(self.groupdb.aliases()))) -class SecretsDbTestCase(unittest.TestCase): +class SecretsDbTestCase(TestCase): + def setUp(self): + super(SecretsDbTestCase, self).setUp() self.secretsdb = SecretsDatabase(os.path.join(DATADIR, "secrets.tdb")) def tearDown(self): self.secretsdb.close() + super(SecretsDbTestCase, self).tearDown() def test_get_sid(self): self.assertTrue(self.secretsdb.get_sid("BEDWYR") is not None) -class TdbSamTestCase(unittest.TestCase): +class TdbSamTestCase(TestCase): + def setUp(self): + super(TdbSamTestCase, self).setUp() self.samdb = TdbSam(os.path.join(DATADIR, "passdb.tdb")) def tearDown(self): self.samdb.close() + super(TdbSamTestCase, self).tearDown() def test_usernames(self): self.assertEquals(3, len(list(self.samdb.usernames()))) @@ -140,8 +161,10 @@ class TdbSamTestCase(unittest.TestCase): self.assertEquals(user, other) -class WinsDatabaseTestCase(unittest.TestCase): +class WinsDatabaseTestCase(TestCase): + def setUp(self): + super(WinsDatabaseTestCase, self).setUp() self.winsdb = WinsDatabase(os.path.join(DATADIR, "wins.dat")) def test_length(self): @@ -152,10 +175,13 @@ class WinsDatabaseTestCase(unittest.TestCase): def tearDown(self): self.winsdb.close() + super(WinsDatabaseTestCase, self).tearDown() -class SmbpasswdTestCase(unittest.TestCase): +class SmbpasswdTestCase(TestCase): + def setUp(self): + super(SmbpasswdTestCase, self).setUp() self.samdb = SmbpasswdFile(os.path.join(DATADIR, "smbpasswd")) def test_length(self): @@ -172,11 +198,15 @@ class SmbpasswdTestCase(unittest.TestCase): def tearDown(self): self.samdb.close() + super(SmbpasswdTestCase, self).tearDown() + +class IdmapDbTestCase(TestCase): -class IdmapDbTestCase(unittest.TestCase): def setUp(self): - self.idmapdb = IdmapDatabase(os.path.join(DATADIR, "winbindd_idmap.tdb")) + super(IdmapDbTestCase, self).setUp() + self.idmapdb = IdmapDatabase(os.path.join(DATADIR, + "winbindd_idmap.tdb")) def test_user_hwm(self): self.assertEquals(10000, self.idmapdb.get_user_hwm()) @@ -198,19 +228,11 @@ class IdmapDbTestCase(unittest.TestCase): def tearDown(self): self.idmapdb.close() + super(IdmapDbTestCase, self).tearDown() -class ShareInfoTestCase(unittest.TestCase): - def setUp(self): - self.shareinfodb = ShareInfoDatabase(os.path.join(DATADIR, "share_info.tdb")) - - # FIXME: needs proper data so it can be tested - - def tearDown(self): - self.shareinfodb.close() - +class ParamTestCase(TestCase): -class ParamTestCase(unittest.TestCase): def test_init(self): file = ParamFile() self.assertTrue(file is not None) diff --git a/source4/scripting/python/samba/tests/samba3sam.py b/source4/scripting/python/samba/tests/samba3sam.py new file mode 100644 index 0000000000..a34f0f620c --- /dev/null +++ b/source4/scripting/python/samba/tests/samba3sam.py @@ -0,0 +1,1096 @@ +#!/usr/bin/env python + +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2005-2008 +# Copyright (C) Martin Kuehl <mkhl@samba.org> 2006 +# +# This is a Python port of the original in testprogs/ejs/samba3sam.js +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +"""Tests for the samba3sam LDB module, which maps Samba3 LDAP to AD LDAP.""" + +import os +import ldb +from ldb import SCOPE_DEFAULT, SCOPE_BASE +from samba import Ldb, substitute_var +from samba.tests import TestCaseInTempDir, env_loadparm +import samba.dcerpc.security +import samba.ndr +from samba.auth import system_session + + +def read_datafile(filename): + paths = [ "../../../../../testdata/samba3", + "../../../../testdata/samba3" ] + for p in paths: + datadir = os.path.join(os.path.dirname(__file__), p) + if os.path.exists(datadir): + break + return open(os.path.join(datadir, filename), 'r').read() + +def ldb_debug(l, text): + print text + + +class MapBaseTestCase(TestCaseInTempDir): + """Base test case for mapping tests.""" + + def setup_modules(self, ldb, s3, s4): + ldb.add({"dn": "@MAP=samba3sam", + "@FROM": s4.basedn, + "@TO": "sambaDomainName=TESTS," + s3.basedn}) + + ldb.add({"dn": "@MODULES", + "@LIST": "rootdse,paged_results,server_sort,asq,samldb,password_hash,operational,objectguid,rdn_name,samba3sam,samba3sid,partition"}) + + ldb.add({"dn": "@PARTITION", + "partition": ["%s" % (s4.basedn_casefold), + "%s" % (s3.basedn_casefold)], + "replicateEntries": ["@ATTRIBUTES", "@INDEXLIST"], + "modules": "*:"}) + + def setUp(self): + self.lp = env_loadparm() + self.lp.set("sid generator", "backend") + self.lp.set("workgroup", "TESTS") + self.lp.set("netbios name", "TESTS") + super(MapBaseTestCase, self).setUp() + + def make_dn(basedn, rdn): + return "%s,sambaDomainName=TESTS,%s" % (rdn, basedn) + + def make_s4dn(basedn, rdn): + return "%s,%s" % (rdn, basedn) + + self.ldbfile = os.path.join(self.tempdir, "test.ldb") + self.ldburl = "tdb://" + self.ldbfile + + tempdir = self.tempdir + + class Target: + """Simple helper class that contains data for a specific SAM + connection.""" + + def __init__(self, basedn, dn, lp): + self.db = Ldb(lp=lp, session_info=system_session()) + self.basedn = basedn + self.basedn_casefold = ldb.Dn(self.db, basedn).get_casefold() + self.substvars = {"BASEDN": self.basedn} + self.file = os.path.join(tempdir, "%s.ldb" % self.basedn_casefold) + self.url = "tdb://" + self.file + self._dn = dn + + def dn(self, rdn): + return self._dn(self.basedn, rdn) + + def connect(self): + return self.db.connect(self.url) + + def setup_data(self, path): + self.add_ldif(read_datafile(path)) + + def subst(self, text): + return substitute_var(text, self.substvars) + + def add_ldif(self, ldif): + self.db.add_ldif(self.subst(ldif)) + + def modify_ldif(self, ldif): + self.db.modify_ldif(self.subst(ldif)) + + self.samba4 = Target("dc=vernstok,dc=nl", make_s4dn, self.lp) + self.samba3 = Target("cn=Samba3Sam", make_dn, self.lp) + + self.samba3.connect() + self.samba4.connect() + + def tearDown(self): + os.unlink(self.ldbfile) + os.unlink(self.samba3.file) + os.unlink(self.samba4.file) + super(MapBaseTestCase, self).tearDown() + + def assertSidEquals(self, text, ndr_sid): + sid_obj1 = samba.ndr.ndr_unpack(samba.dcerpc.security.dom_sid, + str(ndr_sid[0])) + sid_obj2 = samba.dcerpc.security.dom_sid(text) + self.assertEquals(sid_obj1, sid_obj2) + + +class Samba3SamTestCase(MapBaseTestCase): + + def setUp(self): + super(Samba3SamTestCase, self).setUp() + ldb = Ldb(self.ldburl, lp=self.lp, session_info=system_session()) + self.samba3.setup_data("samba3.ldif") + ldif = read_datafile("provision_samba3sam.ldif") + ldb.add_ldif(self.samba4.subst(ldif)) + self.setup_modules(ldb, self.samba3, self.samba4) + del ldb + self.ldb = Ldb(self.ldburl, lp=self.lp, session_info=system_session()) + + def test_search_non_mapped(self): + """Looking up by non-mapped attribute""" + msg = self.ldb.search(expression="(cn=Administrator)") + self.assertEquals(len(msg), 1) + self.assertEquals(msg[0]["cn"], "Administrator") + + def test_search_non_mapped(self): + """Looking up by mapped attribute""" + msg = self.ldb.search(expression="(name=Backup Operators)") + self.assertEquals(len(msg), 1) + self.assertEquals(str(msg[0]["name"]), "Backup Operators") + + def test_old_name_of_renamed(self): + """Looking up by old name of renamed attribute""" + msg = self.ldb.search(expression="(displayName=Backup Operators)") + self.assertEquals(len(msg), 0) + + def test_mapped_containing_sid(self): + """Looking up mapped entry containing SID""" + msg = self.ldb.search(expression="(cn=Replicator)") + self.assertEquals(len(msg), 1) + self.assertEquals(str(msg[0].dn), + "cn=Replicator,ou=Groups,dc=vernstok,dc=nl") + self.assertTrue("objectSid" in msg[0]) + self.assertSidEquals("S-1-5-21-4231626423-2410014848-2360679739-552", + msg[0]["objectSid"]) + oc = set(msg[0]["objectClass"]) + self.assertEquals(oc, set(["group"])) + + def test_search_by_objclass(self): + """Looking up by objectClass""" + msg = self.ldb.search(expression="(|(objectClass=user)(cn=Administrator))") + self.assertEquals(set([str(m.dn) for m in msg]), + set(["unixName=Administrator,ou=Users,dc=vernstok,dc=nl", + "unixName=nobody,ou=Users,dc=vernstok,dc=nl"])) + + def test_s3sam_modify(self): + # Adding a record that will be fallbacked + self.ldb.add({"dn": "cn=Foo", + "foo": "bar", + "blah": "Blie", + "cn": "Foo", + "showInAdvancedViewOnly": "TRUE"} + ) + + # Checking for existence of record (local) + # TODO: This record must be searched in the local database, which is + # currently only supported for base searches + # msg = ldb.search(expression="(cn=Foo)", ['foo','blah','cn','showInAdvancedViewOnly')] + # TODO: Actually, this version should work as well but doesn't... + # + # + msg = self.ldb.search(expression="(cn=Foo)", base="cn=Foo", + scope=SCOPE_BASE, + attrs=['foo','blah','cn','showInAdvancedViewOnly']) + self.assertEquals(len(msg), 1) + self.assertEquals(str(msg[0]["showInAdvancedViewOnly"]), "TRUE") + self.assertEquals(str(msg[0]["foo"]), "bar") + self.assertEquals(str(msg[0]["blah"]), "Blie") + + # Adding record that will be mapped + self.ldb.add({"dn": "cn=Niemand,cn=Users,dc=vernstok,dc=nl", + "objectClass": "user", + "unixName": "bin", + "sambaUnicodePwd": "geheim", + "cn": "Niemand"}) + + # Checking for existence of record (remote) + msg = self.ldb.search(expression="(unixName=bin)", + attrs=['unixName','cn','dn', 'sambaUnicodePwd']) + self.assertEquals(len(msg), 1) + self.assertEquals(str(msg[0]["cn"]), "Niemand") + self.assertEquals(str(msg[0]["sambaUnicodePwd"]), "geheim") + + # Checking for existence of record (local && remote) + msg = self.ldb.search(expression="(&(unixName=bin)(sambaUnicodePwd=geheim))", + attrs=['unixName','cn','dn', 'sambaUnicodePwd']) + self.assertEquals(len(msg), 1) # TODO: should check with more records + self.assertEquals(str(msg[0]["cn"]), "Niemand") + self.assertEquals(str(msg[0]["unixName"]), "bin") + self.assertEquals(str(msg[0]["sambaUnicodePwd"]), "geheim") + + # Checking for existence of record (local || remote) + msg = self.ldb.search(expression="(|(unixName=bin)(sambaUnicodePwd=geheim))", + attrs=['unixName','cn','dn', 'sambaUnicodePwd']) + #print "got %d replies" % len(msg) + self.assertEquals(len(msg), 1) # TODO: should check with more records + self.assertEquals(str(msg[0]["cn"]), "Niemand") + self.assertEquals(str(msg[0]["unixName"]), "bin") + self.assertEquals(str(msg[0]["sambaUnicodePwd"]), "geheim") + + # Checking for data in destination database + msg = self.samba3.db.search(expression="(cn=Niemand)") + self.assertTrue(len(msg) >= 1) + self.assertEquals(str(msg[0]["sambaSID"]), + "S-1-5-21-4231626423-2410014848-2360679739-2001") + self.assertEquals(str(msg[0]["displayName"]), "Niemand") + + # Adding attribute... + self.ldb.modify_ldif(""" +dn: cn=Niemand,cn=Users,dc=vernstok,dc=nl +changetype: modify +add: description +description: Blah +""") + + # Checking whether changes are still there... + msg = self.ldb.search(expression="(cn=Niemand)") + self.assertTrue(len(msg) >= 1) + self.assertEquals(str(msg[0]["cn"]), "Niemand") + self.assertEquals(str(msg[0]["description"]), "Blah") + + # Modifying attribute... + self.ldb.modify_ldif(""" +dn: cn=Niemand,cn=Users,dc=vernstok,dc=nl +changetype: modify +replace: description +description: Blie +""") + + # Checking whether changes are still there... + msg = self.ldb.search(expression="(cn=Niemand)") + self.assertTrue(len(msg) >= 1) + self.assertEquals(str(msg[0]["description"]), "Blie") + + # Deleting attribute... + self.ldb.modify_ldif(""" +dn: cn=Niemand,cn=Users,dc=vernstok,dc=nl +changetype: modify +delete: description +""") + + # Checking whether changes are no longer there... + msg = self.ldb.search(expression="(cn=Niemand)") + self.assertTrue(len(msg) >= 1) + self.assertTrue(not "description" in msg[0]) + + # Renaming record... + self.ldb.rename("cn=Niemand,cn=Users,dc=vernstok,dc=nl", + "cn=Niemand2,cn=Users,dc=vernstok,dc=nl") + + # Checking whether DN has changed... + msg = self.ldb.search(expression="(cn=Niemand2)") + self.assertEquals(len(msg), 1) + self.assertEquals(str(msg[0].dn), + "cn=Niemand2,cn=Users,dc=vernstok,dc=nl") + + # Deleting record... + self.ldb.delete("cn=Niemand2,cn=Users,dc=vernstok,dc=nl") + + # Checking whether record is gone... + msg = self.ldb.search(expression="(cn=Niemand2)") + self.assertEquals(len(msg), 0) + + +class MapTestCase(MapBaseTestCase): + + def setUp(self): + super(MapTestCase, self).setUp() + ldb = Ldb(self.ldburl, lp=self.lp, session_info=system_session()) + ldif = read_datafile("provision_samba3sam.ldif") + ldb.add_ldif(self.samba4.subst(ldif)) + self.setup_modules(ldb, self.samba3, self.samba4) + del ldb + self.ldb = Ldb(self.ldburl, lp=self.lp, session_info=system_session()) + + def test_map_search(self): + """Running search tests on mapped data.""" + self.samba3.db.add({ + "dn": "sambaDomainName=TESTS," + self.samba3.basedn, + "objectclass": ["sambaDomain", "top"], + "sambaSID": "S-1-5-21-4231626423-2410014848-2360679739", + "sambaNextRid": "2000", + "sambaDomainName": "TESTS" + }) + + # Add a set of split records + self.ldb.add_ldif(""" +dn: """+ self.samba4.dn("cn=Domain Users") + """ +objectClass: group +cn: Domain Users +objectSid: S-1-5-21-4231626423-2410014848-2360679739-513 +""") + + # Add a set of split records + self.ldb.add_ldif(""" +dn: """+ self.samba4.dn("cn=X") + """ +objectClass: user +cn: X +codePage: x +revision: x +dnsHostName: x +nextRid: y +lastLogon: x +description: x +objectSid: S-1-5-21-4231626423-2410014848-2360679739-552 +""") + + self.ldb.add({ + "dn": self.samba4.dn("cn=Y"), + "objectClass": "top", + "cn": "Y", + "codePage": "x", + "revision": "x", + "dnsHostName": "y", + "nextRid": "y", + "lastLogon": "y", + "description": "x"}) + + self.ldb.add({ + "dn": self.samba4.dn("cn=Z"), + "objectClass": "top", + "cn": "Z", + "codePage": "x", + "revision": "y", + "dnsHostName": "z", + "nextRid": "y", + "lastLogon": "z", + "description": "y"}) + + # Add a set of remote records + + self.samba3.db.add({ + "dn": self.samba3.dn("cn=A"), + "objectClass": "posixAccount", + "cn": "A", + "sambaNextRid": "x", + "sambaBadPasswordCount": "x", + "sambaLogonTime": "x", + "description": "x", + "sambaSID": "S-1-5-21-4231626423-2410014848-2360679739-552", + "sambaPrimaryGroupSID": "S-1-5-21-4231626423-2410014848-2360679739-512"}) + + self.samba3.db.add({ + "dn": self.samba3.dn("cn=B"), + "objectClass": "top", + "cn": "B", + "sambaNextRid": "x", + "sambaBadPasswordCount": "x", + "sambaLogonTime": "y", + "description": "x"}) + + self.samba3.db.add({ + "dn": self.samba3.dn("cn=C"), + "objectClass": "top", + "cn": "C", + "sambaNextRid": "x", + "sambaBadPasswordCount": "y", + "sambaLogonTime": "z", + "description": "y"}) + + # Testing search by DN + + # Search remote record by local DN + dn = self.samba4.dn("cn=A") + res = self.ldb.search(dn, scope=SCOPE_BASE, + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertEquals(str(res[0]["lastLogon"]), "x") + + # Search remote record by remote DN + dn = self.samba3.dn("cn=A") + res = self.samba3.db.search(dn, scope=SCOPE_BASE, + attrs=["dnsHostName", "lastLogon", "sambaLogonTime"]) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertTrue(not "lastLogon" in res[0]) + self.assertEquals(str(res[0]["sambaLogonTime"]), "x") + + # Search split record by local DN + dn = self.samba4.dn("cn=X") + res = self.ldb.search(dn, scope=SCOPE_BASE, + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertEquals(str(res[0]["dnsHostName"]), "x") + self.assertEquals(str(res[0]["lastLogon"]), "x") + + # Search split record by remote DN + dn = self.samba3.dn("cn=X") + res = self.samba3.db.search(dn, scope=SCOPE_BASE, + attrs=["dnsHostName", "lastLogon", "sambaLogonTime"]) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertTrue(not "lastLogon" in res[0]) + self.assertEquals(str(res[0]["sambaLogonTime"]), "x") + + # Testing search by attribute + + # Search by ignored attribute + res = self.ldb.search(expression="(revision=x)", scope=SCOPE_DEFAULT, + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 2) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=Y")) + self.assertEquals(str(res[0]["dnsHostName"]), "y") + self.assertEquals(str(res[0]["lastLogon"]), "y") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=X")) + self.assertEquals(str(res[1]["dnsHostName"]), "x") + self.assertEquals(str(res[1]["lastLogon"]), "x") + + # Search by kept attribute + res = self.ldb.search(expression="(description=y)", + scope=SCOPE_DEFAULT, attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 2) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=Z")) + self.assertEquals(str(res[0]["dnsHostName"]), "z") + self.assertEquals(str(res[0]["lastLogon"]), "z") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=C")) + self.assertTrue(not "dnsHostName" in res[1]) + self.assertEquals(str(res[1]["lastLogon"]), "z") + + # Search by renamed attribute + res = self.ldb.search(expression="(badPwdCount=x)", scope=SCOPE_DEFAULT, + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 2) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=B")) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertEquals(str(res[0]["lastLogon"]), "y") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=A")) + self.assertTrue(not "dnsHostName" in res[1]) + self.assertEquals(str(res[1]["lastLogon"]), "x") + + # Search by converted attribute + # TODO: + # Using the SID directly in the parse tree leads to conversion + # errors, letting the search fail with no results. + #res = self.ldb.search("(objectSid=S-1-5-21-4231626423-2410014848-2360679739-552)", scope=SCOPE_DEFAULT, attrs) + res = self.ldb.search(expression="(objectSid=*)", base=None, scope=SCOPE_DEFAULT, attrs=["dnsHostName", "lastLogon", "objectSid"]) + self.assertEquals(len(res), 4) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=X")) + self.assertEquals(str(res[0]["dnsHostName"]), "x") + self.assertEquals(str(res[0]["lastLogon"]), "x") + self.assertSidEquals("S-1-5-21-4231626423-2410014848-2360679739-552", + res[0]["objectSid"]) + self.assertTrue("objectSid" in res[0]) + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=A")) + self.assertTrue(not "dnsHostName" in res[1]) + self.assertEquals(str(res[1]["lastLogon"]), "x") + self.assertSidEquals("S-1-5-21-4231626423-2410014848-2360679739-552", + res[1]["objectSid"]) + self.assertTrue("objectSid" in res[1]) + + # Search by generated attribute + # In most cases, this even works when the mapping is missing + # a `convert_operator' by enumerating the remote db. + res = self.ldb.search(expression="(primaryGroupID=512)", + attrs=["dnsHostName", "lastLogon", "primaryGroupID"]) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=A")) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertEquals(str(res[0]["lastLogon"]), "x") + self.assertEquals(str(res[0]["primaryGroupID"]), "512") + + # Note that Xs "objectSid" seems to be fine in the previous search for + # "objectSid"... + #res = ldb.search(expression="(primaryGroupID=*)", NULL, ldb. SCOPE_DEFAULT, attrs) + #print len(res) + " results found" + #for i in range(len(res)): + # for (obj in res[i]) { + # print obj + ": " + res[i][obj] + # } + # print "---" + # + + # Search by remote name of renamed attribute */ + res = self.ldb.search(expression="(sambaBadPasswordCount=*)", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 0) + + # Search by objectClass + attrs = ["dnsHostName", "lastLogon", "objectClass"] + res = self.ldb.search(expression="(objectClass=user)", attrs=attrs) + self.assertEquals(len(res), 2) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=X")) + self.assertEquals(str(res[0]["dnsHostName"]), "x") + self.assertEquals(str(res[0]["lastLogon"]), "x") + self.assertEquals(str(res[0]["objectClass"][0]), "user") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=A")) + self.assertTrue(not "dnsHostName" in res[1]) + self.assertEquals(str(res[1]["lastLogon"]), "x") + self.assertEquals(str(res[1]["objectClass"][0]), "user") + + # Prove that the objectClass is actually used for the search + res = self.ldb.search(expression="(|(objectClass=user)(badPwdCount=x))", + attrs=attrs) + self.assertEquals(len(res), 3) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=B")) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertEquals(str(res[0]["lastLogon"]), "y") + self.assertEquals(set(res[0]["objectClass"]), set(["top"])) + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=X")) + self.assertEquals(str(res[1]["dnsHostName"]), "x") + self.assertEquals(str(res[1]["lastLogon"]), "x") + self.assertEquals(str(res[1]["objectClass"][0]), "user") + self.assertEquals(str(res[2].dn), self.samba4.dn("cn=A")) + self.assertTrue(not "dnsHostName" in res[2]) + self.assertEquals(str(res[2]["lastLogon"]), "x") + self.assertEquals(res[2]["objectClass"][0], "user") + + # Testing search by parse tree + + # Search by conjunction of local attributes + res = self.ldb.search(expression="(&(codePage=x)(revision=x))", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 2) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=Y")) + self.assertEquals(str(res[0]["dnsHostName"]), "y") + self.assertEquals(str(res[0]["lastLogon"]), "y") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=X")) + self.assertEquals(str(res[1]["dnsHostName"]), "x") + self.assertEquals(str(res[1]["lastLogon"]), "x") + + # Search by conjunction of remote attributes + res = self.ldb.search(expression="(&(lastLogon=x)(description=x))", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 2) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=X")) + self.assertEquals(str(res[0]["dnsHostName"]), "x") + self.assertEquals(str(res[0]["lastLogon"]), "x") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=A")) + self.assertTrue(not "dnsHostName" in res[1]) + self.assertEquals(str(res[1]["lastLogon"]), "x") + + # Search by conjunction of local and remote attribute + res = self.ldb.search(expression="(&(codePage=x)(description=x))", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 2) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=Y")) + self.assertEquals(str(res[0]["dnsHostName"]), "y") + self.assertEquals(str(res[0]["lastLogon"]), "y") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=X")) + self.assertEquals(str(res[1]["dnsHostName"]), "x") + self.assertEquals(str(res[1]["lastLogon"]), "x") + + # Search by conjunction of local and remote attribute w/o match + attrs = ["dnsHostName", "lastLogon"] + res = self.ldb.search(expression="(&(codePage=x)(nextRid=x))", + attrs=attrs) + self.assertEquals(len(res), 0) + res = self.ldb.search(expression="(&(revision=x)(lastLogon=z))", + attrs=attrs) + self.assertEquals(len(res), 0) + + # Search by disjunction of local attributes + res = self.ldb.search(expression="(|(revision=x)(dnsHostName=x))", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 2) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=Y")) + self.assertEquals(str(res[0]["dnsHostName"]), "y") + self.assertEquals(str(res[0]["lastLogon"]), "y") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=X")) + self.assertEquals(str(res[1]["dnsHostName"]), "x") + self.assertEquals(str(res[1]["lastLogon"]), "x") + + # Search by disjunction of remote attributes + res = self.ldb.search(expression="(|(badPwdCount=x)(lastLogon=x))", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 3) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=B")) + self.assertFalse("dnsHostName" in res[0]) + self.assertEquals(str(res[0]["lastLogon"]), "y") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=X")) + self.assertEquals(str(res[1]["dnsHostName"]), "x") + self.assertEquals(str(res[1]["lastLogon"]), "x") + self.assertEquals(str(res[2].dn), self.samba4.dn("cn=A")) + self.assertFalse("dnsHostName" in res[2]) + self.assertEquals(str(res[2]["lastLogon"]), "x") + + # Search by disjunction of local and remote attribute + res = self.ldb.search(expression="(|(revision=x)(lastLogon=y))", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 3) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=Y")) + self.assertEquals(str(res[0]["dnsHostName"]), "y") + self.assertEquals(str(res[0]["lastLogon"]), "y") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=B")) + self.assertFalse("dnsHostName" in res[1]) + self.assertEquals(str(res[1]["lastLogon"]), "y") + self.assertEquals(str(res[2].dn), self.samba4.dn("cn=X")) + self.assertEquals(str(res[2]["dnsHostName"]), "x") + self.assertEquals(str(res[2]["lastLogon"]), "x") + + # Search by disjunction of local and remote attribute w/o match + res = self.ldb.search(expression="(|(codePage=y)(nextRid=z))", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 0) + + # Search by negated local attribute + res = self.ldb.search(expression="(!(revision=x))", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 6) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=B")) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertEquals(str(res[0]["lastLogon"]), "y") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=A")) + self.assertTrue(not "dnsHostName" in res[1]) + self.assertEquals(str(res[1]["lastLogon"]), "x") + self.assertEquals(str(res[2].dn), self.samba4.dn("cn=Z")) + self.assertEquals(str(res[2]["dnsHostName"]), "z") + self.assertEquals(str(res[2]["lastLogon"]), "z") + self.assertEquals(str(res[3].dn), self.samba4.dn("cn=C")) + self.assertTrue(not "dnsHostName" in res[3]) + self.assertEquals(str(res[3]["lastLogon"]), "z") + + # Search by negated remote attribute + res = self.ldb.search(expression="(!(description=x))", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 4) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=Z")) + self.assertEquals(str(res[0]["dnsHostName"]), "z") + self.assertEquals(str(res[0]["lastLogon"]), "z") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=C")) + self.assertTrue(not "dnsHostName" in res[1]) + self.assertEquals(str(res[1]["lastLogon"]), "z") + + # Search by negated conjunction of local attributes + res = self.ldb.search(expression="(!(&(codePage=x)(revision=x)))", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 6) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=B")) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertEquals(str(res[0]["lastLogon"]), "y") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=A")) + self.assertTrue(not "dnsHostName" in res[1]) + self.assertEquals(str(res[1]["lastLogon"]), "x") + self.assertEquals(str(res[2].dn), self.samba4.dn("cn=Z")) + self.assertEquals(str(res[2]["dnsHostName"]), "z") + self.assertEquals(str(res[2]["lastLogon"]), "z") + self.assertEquals(str(res[3].dn), self.samba4.dn("cn=C")) + self.assertTrue(not "dnsHostName" in res[3]) + self.assertEquals(str(res[3]["lastLogon"]), "z") + + # Search by negated conjunction of remote attributes + res = self.ldb.search(expression="(!(&(lastLogon=x)(description=x)))", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 6) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=Y")) + self.assertEquals(str(res[0]["dnsHostName"]), "y") + self.assertEquals(str(res[0]["lastLogon"]), "y") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=B")) + self.assertTrue(not "dnsHostName" in res[1]) + self.assertEquals(str(res[1]["lastLogon"]), "y") + self.assertEquals(str(res[2].dn), self.samba4.dn("cn=Z")) + self.assertEquals(str(res[2]["dnsHostName"]), "z") + self.assertEquals(str(res[2]["lastLogon"]), "z") + self.assertEquals(str(res[3].dn), self.samba4.dn("cn=C")) + self.assertTrue(not "dnsHostName" in res[3]) + self.assertEquals(str(res[3]["lastLogon"]), "z") + + # Search by negated conjunction of local and remote attribute + res = self.ldb.search(expression="(!(&(codePage=x)(description=x)))", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 6) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=B")) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertEquals(str(res[0]["lastLogon"]), "y") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=A")) + self.assertTrue(not "dnsHostName" in res[1]) + self.assertEquals(str(res[1]["lastLogon"]), "x") + self.assertEquals(str(res[2].dn), self.samba4.dn("cn=Z")) + self.assertEquals(str(res[2]["dnsHostName"]), "z") + self.assertEquals(str(res[2]["lastLogon"]), "z") + self.assertEquals(str(res[3].dn), self.samba4.dn("cn=C")) + self.assertTrue(not "dnsHostName" in res[3]) + self.assertEquals(str(res[3]["lastLogon"]), "z") + + # Search by negated disjunction of local attributes + res = self.ldb.search(expression="(!(|(revision=x)(dnsHostName=x)))", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=B")) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertEquals(str(res[0]["lastLogon"]), "y") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=A")) + self.assertTrue(not "dnsHostName" in res[1]) + self.assertEquals(str(res[1]["lastLogon"]), "x") + self.assertEquals(str(res[2].dn), self.samba4.dn("cn=Z")) + self.assertEquals(str(res[2]["dnsHostName"]), "z") + self.assertEquals(str(res[2]["lastLogon"]), "z") + self.assertEquals(str(res[3].dn), self.samba4.dn("cn=C")) + self.assertTrue(not "dnsHostName" in res[3]) + self.assertEquals(str(res[3]["lastLogon"]), "z") + + # Search by negated disjunction of remote attributes + res = self.ldb.search(expression="(!(|(badPwdCount=x)(lastLogon=x)))", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 5) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=Y")) + self.assertEquals(str(res[0]["dnsHostName"]), "y") + self.assertEquals(str(res[0]["lastLogon"]), "y") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=Z")) + self.assertEquals(str(res[1]["dnsHostName"]), "z") + self.assertEquals(str(res[1]["lastLogon"]), "z") + self.assertEquals(str(res[2].dn), self.samba4.dn("cn=C")) + self.assertTrue(not "dnsHostName" in res[2]) + self.assertEquals(str(res[2]["lastLogon"]), "z") + + # Search by negated disjunction of local and remote attribute + res = self.ldb.search(expression="(!(|(revision=x)(lastLogon=y)))", + attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 5) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=A")) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertEquals(str(res[0]["lastLogon"]), "x") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=Z")) + self.assertEquals(str(res[1]["dnsHostName"]), "z") + self.assertEquals(str(res[1]["lastLogon"]), "z") + self.assertEquals(str(res[2].dn), self.samba4.dn("cn=C")) + self.assertTrue(not "dnsHostName" in res[2]) + self.assertEquals(str(res[2]["lastLogon"]), "z") + + # Search by complex parse tree + res = self.ldb.search(expression="(|(&(revision=x)(dnsHostName=x))(!(&(description=x)(nextRid=y)))(badPwdCount=y))", attrs=["dnsHostName", "lastLogon"]) + self.assertEquals(len(res), 7) + self.assertEquals(str(res[0].dn), self.samba4.dn("cn=B")) + self.assertTrue(not "dnsHostName" in res[0]) + self.assertEquals(str(res[0]["lastLogon"]), "y") + self.assertEquals(str(res[1].dn), self.samba4.dn("cn=X")) + self.assertEquals(str(res[1]["dnsHostName"]), "x") + self.assertEquals(str(res[1]["lastLogon"]), "x") + self.assertEquals(str(res[2].dn), self.samba4.dn("cn=A")) + self.assertTrue(not "dnsHostName" in res[2]) + self.assertEquals(str(res[2]["lastLogon"]), "x") + self.assertEquals(str(res[3].dn), self.samba4.dn("cn=Z")) + self.assertEquals(str(res[3]["dnsHostName"]), "z") + self.assertEquals(str(res[3]["lastLogon"]), "z") + self.assertEquals(str(res[4].dn), self.samba4.dn("cn=C")) + self.assertTrue(not "dnsHostName" in res[4]) + self.assertEquals(str(res[4]["lastLogon"]), "z") + + # Clean up + dns = [self.samba4.dn("cn=%s" % n) for n in ["A","B","C","X","Y","Z"]] + for dn in dns: + self.ldb.delete(dn) + + def test_map_modify_local(self): + """Modification of local records.""" + # Add local record + dn = "cn=test,dc=idealx,dc=org" + self.ldb.add({"dn": dn, + "cn": "test", + "foo": "bar", + "revision": "1", + "description": "test"}) + # Check it's there + attrs = ["foo", "revision", "description"] + res = self.ldb.search(dn, scope=SCOPE_BASE, attrs=attrs) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertEquals(str(res[0]["foo"]), "bar") + self.assertEquals(str(res[0]["revision"]), "1") + self.assertEquals(str(res[0]["description"]), "test") + # Check it's not in the local db + res = self.samba4.db.search(expression="(cn=test)", + scope=SCOPE_DEFAULT, attrs=attrs) + self.assertEquals(len(res), 0) + # Check it's not in the remote db + res = self.samba3.db.search(expression="(cn=test)", + scope=SCOPE_DEFAULT, attrs=attrs) + self.assertEquals(len(res), 0) + + # Modify local record + ldif = """ +dn: """ + dn + """ +replace: foo +foo: baz +replace: description +description: foo +""" + self.ldb.modify_ldif(ldif) + # Check in local db + res = self.ldb.search(dn, scope=SCOPE_BASE, attrs=attrs) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertEquals(str(res[0]["foo"]), "baz") + self.assertEquals(str(res[0]["revision"]), "1") + self.assertEquals(str(res[0]["description"]), "foo") + + # Rename local record + dn2 = "cn=toast,dc=idealx,dc=org" + self.ldb.rename(dn, dn2) + # Check in local db + res = self.ldb.search(dn2, scope=SCOPE_BASE, attrs=attrs) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn2) + self.assertEquals(str(res[0]["foo"]), "baz") + self.assertEquals(str(res[0]["revision"]), "1") + self.assertEquals(str(res[0]["description"]), "foo") + + # Delete local record + self.ldb.delete(dn2) + # Check it's gone + res = self.ldb.search(dn2, scope=SCOPE_BASE) + self.assertEquals(len(res), 0) + + def test_map_modify_remote_remote(self): + """Modification of remote data of remote records""" + # Add remote record + dn = self.samba4.dn("cn=test") + dn2 = self.samba3.dn("cn=test") + self.samba3.db.add({"dn": dn2, + "cn": "test", + "description": "foo", + "sambaBadPasswordCount": "3", + "sambaNextRid": "1001"}) + # Check it's there + res = self.samba3.db.search(dn2, scope=SCOPE_BASE, + attrs=["description", "sambaBadPasswordCount", "sambaNextRid"]) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn2) + self.assertEquals(str(res[0]["description"]), "foo") + self.assertEquals(str(res[0]["sambaBadPasswordCount"]), "3") + self.assertEquals(str(res[0]["sambaNextRid"]), "1001") + # Check in mapped db + attrs = ["description", "badPwdCount", "nextRid"] + res = self.ldb.search(dn, scope=SCOPE_BASE, attrs=attrs, expression="") + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertEquals(str(res[0]["description"]), "foo") + self.assertEquals(str(res[0]["badPwdCount"]), "3") + self.assertEquals(str(res[0]["nextRid"]), "1001") + # Check in local db + res = self.samba4.db.search(dn, scope=SCOPE_BASE, attrs=attrs) + self.assertEquals(len(res), 0) + + # Modify remote data of remote record + ldif = """ +dn: """ + dn + """ +replace: description +description: test +replace: badPwdCount +badPwdCount: 4 +""" + self.ldb.modify_ldif(ldif) + # Check in mapped db + res = self.ldb.search(dn, scope=SCOPE_BASE, + attrs=["description", "badPwdCount", "nextRid"]) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertEquals(str(res[0]["description"]), "test") + self.assertEquals(str(res[0]["badPwdCount"]), "4") + self.assertEquals(str(res[0]["nextRid"]), "1001") + # Check in remote db + res = self.samba3.db.search(dn2, scope=SCOPE_BASE, + attrs=["description", "sambaBadPasswordCount", "sambaNextRid"]) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn2) + self.assertEquals(str(res[0]["description"]), "test") + self.assertEquals(str(res[0]["sambaBadPasswordCount"]), "4") + self.assertEquals(str(res[0]["sambaNextRid"]), "1001") + + # Rename remote record + dn2 = self.samba4.dn("cn=toast") + self.ldb.rename(dn, dn2) + # Check in mapped db + dn = dn2 + res = self.ldb.search(dn, scope=SCOPE_BASE, + attrs=["description", "badPwdCount", "nextRid"]) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertEquals(str(res[0]["description"]), "test") + self.assertEquals(str(res[0]["badPwdCount"]), "4") + self.assertEquals(str(res[0]["nextRid"]), "1001") + # Check in remote db + dn2 = self.samba3.dn("cn=toast") + res = self.samba3.db.search(dn2, scope=SCOPE_BASE, + attrs=["description", "sambaBadPasswordCount", "sambaNextRid"]) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn2) + self.assertEquals(str(res[0]["description"]), "test") + self.assertEquals(str(res[0]["sambaBadPasswordCount"]), "4") + self.assertEquals(str(res[0]["sambaNextRid"]), "1001") + + # Delete remote record + self.ldb.delete(dn) + # Check in mapped db that it's removed + res = self.ldb.search(dn, scope=SCOPE_BASE) + self.assertEquals(len(res), 0) + # Check in remote db + res = self.samba3.db.search(dn2, scope=SCOPE_BASE) + self.assertEquals(len(res), 0) + + def test_map_modify_remote_local(self): + """Modification of local data of remote records""" + # Add remote record (same as before) + dn = self.samba4.dn("cn=test") + dn2 = self.samba3.dn("cn=test") + self.samba3.db.add({"dn": dn2, + "cn": "test", + "description": "foo", + "sambaBadPasswordCount": "3", + "sambaNextRid": "1001"}) + + # Modify local data of remote record + ldif = """ +dn: """ + dn + """ +add: revision +revision: 1 +replace: description +description: test + +""" + self.ldb.modify_ldif(ldif) + # Check in mapped db + attrs = ["revision", "description"] + res = self.ldb.search(dn, scope=SCOPE_BASE, attrs=attrs) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertEquals(str(res[0]["description"]), "test") + self.assertEquals(str(res[0]["revision"]), "1") + # Check in remote db + res = self.samba3.db.search(dn2, scope=SCOPE_BASE, attrs=attrs) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn2) + self.assertEquals(str(res[0]["description"]), "test") + self.assertTrue(not "revision" in res[0]) + # Check in local db + res = self.samba4.db.search(dn, scope=SCOPE_BASE, attrs=attrs) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertTrue(not "description" in res[0]) + self.assertEquals(str(res[0]["revision"]), "1") + + # Delete (newly) split record + self.ldb.delete(dn) + + def test_map_modify_split(self): + """Testing modification of split records""" + # Add split record + dn = self.samba4.dn("cn=test") + dn2 = self.samba3.dn("cn=test") + self.ldb.add({ + "dn": dn, + "cn": "test", + "description": "foo", + "badPwdCount": "3", + "nextRid": "1001", + "revision": "1"}) + # Check it's there + attrs = ["description", "badPwdCount", "nextRid", "revision"] + res = self.ldb.search(dn, scope=SCOPE_BASE, attrs=attrs) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertEquals(str(res[0]["description"]), "foo") + self.assertEquals(str(res[0]["badPwdCount"]), "3") + self.assertEquals(str(res[0]["nextRid"]), "1001") + self.assertEquals(str(res[0]["revision"]), "1") + # Check in local db + res = self.samba4.db.search(dn, scope=SCOPE_BASE, attrs=attrs) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertTrue(not "description" in res[0]) + self.assertTrue(not "badPwdCount" in res[0]) + self.assertTrue(not "nextRid" in res[0]) + self.assertEquals(str(res[0]["revision"]), "1") + # Check in remote db + attrs = ["description", "sambaBadPasswordCount", "sambaNextRid", + "revision"] + res = self.samba3.db.search(dn2, scope=SCOPE_BASE, attrs=attrs) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn2) + self.assertEquals(str(res[0]["description"]), "foo") + self.assertEquals(str(res[0]["sambaBadPasswordCount"]), "3") + self.assertEquals(str(res[0]["sambaNextRid"]), "1001") + self.assertTrue(not "revision" in res[0]) + + # Modify of split record + ldif = """ +dn: """ + dn + """ +replace: description +description: test +replace: badPwdCount +badPwdCount: 4 +replace: revision +revision: 2 +""" + self.ldb.modify_ldif(ldif) + # Check in mapped db + attrs = ["description", "badPwdCount", "nextRid", "revision"] + res = self.ldb.search(dn, scope=SCOPE_BASE, attrs=attrs) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertEquals(str(res[0]["description"]), "test") + self.assertEquals(str(res[0]["badPwdCount"]), "4") + self.assertEquals(str(res[0]["nextRid"]), "1001") + self.assertEquals(str(res[0]["revision"]), "2") + # Check in local db + res = self.samba4.db.search(dn, scope=SCOPE_BASE, attrs=attrs) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertTrue(not "description" in res[0]) + self.assertTrue(not "badPwdCount" in res[0]) + self.assertTrue(not "nextRid" in res[0]) + self.assertEquals(str(res[0]["revision"]), "2") + # Check in remote db + attrs = ["description", "sambaBadPasswordCount", "sambaNextRid", + "revision"] + res = self.samba3.db.search(dn2, scope=SCOPE_BASE, attrs=attrs) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn2) + self.assertEquals(str(res[0]["description"]), "test") + self.assertEquals(str(res[0]["sambaBadPasswordCount"]), "4") + self.assertEquals(str(res[0]["sambaNextRid"]), "1001") + self.assertTrue(not "revision" in res[0]) + + # Rename split record + dn2 = self.samba4.dn("cn=toast") + self.ldb.rename(dn, dn2) + # Check in mapped db + dn = dn2 + attrs = ["description", "badPwdCount", "nextRid", "revision"] + res = self.ldb.search(dn, scope=SCOPE_BASE, attrs=attrs) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertEquals(str(res[0]["description"]), "test") + self.assertEquals(str(res[0]["badPwdCount"]), "4") + self.assertEquals(str(res[0]["nextRid"]), "1001") + self.assertEquals(str(res[0]["revision"]), "2") + # Check in local db + res = self.samba4.db.search(dn, scope=SCOPE_BASE, attrs=attrs) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn) + self.assertTrue(not "description" in res[0]) + self.assertTrue(not "badPwdCount" in res[0]) + self.assertTrue(not "nextRid" in res[0]) + self.assertEquals(str(res[0]["revision"]), "2") + # Check in remote db + dn2 = self.samba3.dn("cn=toast") + res = self.samba3.db.search(dn2, scope=SCOPE_BASE, + attrs=["description", "sambaBadPasswordCount", "sambaNextRid", + "revision"]) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0].dn), dn2) + self.assertEquals(str(res[0]["description"]), "test") + self.assertEquals(str(res[0]["sambaBadPasswordCount"]), "4") + self.assertEquals(str(res[0]["sambaNextRid"]), "1001") + self.assertTrue(not "revision" in res[0]) + + # Delete split record + self.ldb.delete(dn) + # Check in mapped db + res = self.ldb.search(dn, scope=SCOPE_BASE) + self.assertEquals(len(res), 0) + # Check in local db + res = self.samba4.db.search(dn, scope=SCOPE_BASE) + self.assertEquals(len(res), 0) + # Check in remote db + res = self.samba3.db.search(dn2, scope=SCOPE_BASE) + self.assertEquals(len(res), 0) diff --git a/source4/scripting/python/samba/tests/samdb.py b/source4/scripting/python/samba/tests/samdb.py index 8c7bb0ae98..3df72b0840 100644 --- a/source4/scripting/python/samba/tests/samdb.py +++ b/source4/scripting/python/samba/tests/samdb.py @@ -1,43 +1,46 @@ -#!/usr/bin/python +#!/usr/bin/env python # Unix SMB/CIFS implementation. Tests for SamDB # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2008 -# +# # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. -# +# # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # -from samba.auth import system_session -from samba.credentials import Credentials + +"""Tests for samba.samdb.""" + +import logging import os -from samba.provision import setup_samdb, guess_names, setup_templatesdb, make_smbconf, find_setup_dir -from samba.samdb import SamDB +import uuid + +from samba.auth import system_session +from samba.provision import (setup_samdb, guess_names, make_smbconf, + provision_paths_from_lp) +from samba.provision import DEFAULT_POLICY_GUID, DEFAULT_DC_POLICY_GUID +from samba.provision.backend import ProvisionBackend from samba.tests import TestCaseInTempDir from samba.dcerpc import security -from unittest import TestCase -import uuid +from samba.schema import Schema from samba import param class SamDBTestCase(TestCaseInTempDir): """Base-class for tests with a Sam Database. - + This is used by the Samba SamDB-tests, but e.g. also by the OpenChange provisioning tests (which need a Sam). """ - def setup_path(self, relpath): - return os.path.join(find_setup_dir(), relpath) - def setUp(self): super(SamDBTestCase, self).setUp() invocationid = str(uuid.uuid4()) @@ -46,11 +49,8 @@ class SamDBTestCase(TestCaseInTempDir): configdn = "CN=Configuration," + domaindn schemadn = "CN=Schema," + configdn domainguid = str(uuid.uuid4()) - policyguid = str(uuid.uuid4()) - creds = Credentials() - creds.set_anonymous() + policyguid = DEFAULT_POLICY_GUID domainsid = security.random_sid() - hostguid = str(uuid.uuid4()) path = os.path.join(self.tempdir, "samdb.ldb") session_info = system_session() @@ -58,9 +58,10 @@ class SamDBTestCase(TestCaseInTempDir): domain="EXAMPLE" dnsdomain="example.com" serverrole="domain controller" + policyguid_dc = DEFAULT_DC_POLICY_GUID smbconf = os.path.join(self.tempdir, "smb.conf") - make_smbconf(smbconf, self.setup_path, hostname, domain, dnsdomain, + make_smbconf(smbconf, hostname, domain, dnsdomain, serverrole, self.tempdir) self.lp = param.LoadParm() @@ -71,26 +72,27 @@ class SamDBTestCase(TestCaseInTempDir): serverrole=serverrole, domaindn=self.domaindn, configdn=configdn, schemadn=schemadn) - setup_templatesdb(os.path.join(self.tempdir, "templates.ldb"), - 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, - domainguid, - policyguid, False, "secret", - "secret", "secret", invocationid, - "secret", "domain controller") + + paths = provision_paths_from_lp(self.lp, names.dnsdomain) + + logger = logging.getLogger("provision") + + provision_backend = ProvisionBackend("ldb", paths=paths, + lp=self.lp, credentials=None, + names=names, logger=logger) + + schema = Schema(domainsid, invocationid=invocationid, + schemadn=names.schemadn, serverdn=names.serverdn, + am_rodc=False) + + self.samdb = setup_samdb(path, session_info, + provision_backend, self.lp, names, logger, + domainsid, domainguid, policyguid, policyguid_dc, False, + "secret", "secret", "secret", invocationid, "secret", + None, "domain controller", schema=schema) def tearDown(self): - for f in ['templates.ldb', 'schema.ldb', 'configuration.ldb', + for f in ['schema.ldb', 'configuration.ldb', 'users.ldb', 'samdb.ldb', 'smb.conf']: os.remove(os.path.join(self.tempdir, f)) super(SamDBTestCase, self).tearDown() - - -# disable this test till andrew works it out ... -class SamDBTests(SamDBTestCase): - """Tests for the SamDB implementation.""" - - print "samdb add_foreign disabled for now" -# def test_add_foreign(self): diff --git a/source4/scripting/python/samba/tests/security.py b/source4/scripting/python/samba/tests/security.py new file mode 100644 index 0000000000..59e3113068 --- /dev/null +++ b/source4/scripting/python/samba/tests/security.py @@ -0,0 +1,145 @@ +#!/usr/bin/env 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/>. +# + +"""Tests for samba.dcerpc.security.""" + +import samba.tests +from samba.dcerpc import security + +class SecurityTokenTests(samba.tests.TestCase): + + def setUp(self): + super(SecurityTokenTests, self).setUp() + self.token = security.token() + + def test_is_system(self): + self.assertFalse(self.token.is_system()) + + def test_is_anonymous(self): + self.assertFalse(self.token.is_anonymous()) + + def test_has_builtin_administrators(self): + self.assertFalse(self.token.has_builtin_administrators()) + + def test_has_nt_authenticated_users(self): + self.assertFalse(self.token.has_nt_authenticated_users()) + + def test_has_priv(self): + self.assertFalse(self.token.has_privilege(security.SEC_PRIV_SHUTDOWN)) + + def test_set_priv(self): + self.assertFalse(self.token.has_privilege(security.SEC_PRIV_SHUTDOWN)) + self.assertFalse(self.token.set_privilege(security.SEC_PRIV_SHUTDOWN)) + self.assertTrue(self.token.has_privilege(security.SEC_PRIV_SHUTDOWN)) + + +class SecurityDescriptorTests(samba.tests.TestCase): + + def setUp(self): + super(SecurityDescriptorTests, self).setUp() + self.descriptor = security.descriptor() + + def test_from_sddl(self): + desc = security.descriptor.from_sddl("O:AOG:DAD:(A;;RPWPCCDCLCSWRCWDWOGA;;;S-1-0-0)", security.dom_sid("S-2-0-0")) + self.assertEquals(desc.group_sid, security.dom_sid('S-2-0-0-512')) + self.assertEquals(desc.owner_sid, security.dom_sid('S-1-5-32-548')) + self.assertEquals(desc.revision, 1) + self.assertEquals(desc.sacl, None) + self.assertEquals(desc.type, 0x8004) + + def test_from_sddl_invalidsddl(self): + self.assertRaises(TypeError,security.descriptor.from_sddl, "foo",security.dom_sid("S-2-0-0")) + + def test_from_sddl_invalidtype1(self): + self.assertRaises(TypeError, security.descriptor.from_sddl, security.dom_sid('S-2-0-0-512'),security.dom_sid("S-2-0-0")) + + def test_from_sddl_invalidtype2(self): + sddl = "O:AOG:DAD:(A;;RPWPCCDCLCSWRCWDWOGA;;;S-1-0-0)" + self.assertRaises(TypeError, security.descriptor.from_sddl, sddl, + "S-2-0-0") + + def test_as_sddl(self): + text = "O:AOG:DAD:(A;;RPWPCCDCLCSWRCWDWOGA;;;S-1-0-0)" + dom = security.dom_sid("S-2-0-0") + desc1 = security.descriptor.from_sddl(text, dom) + desc2 = security.descriptor.from_sddl(desc1.as_sddl(dom), dom) + self.assertEquals(desc1.group_sid, desc2.group_sid) + self.assertEquals(desc1.owner_sid, desc2.owner_sid) + self.assertEquals(desc1.sacl, desc2.sacl) + self.assertEquals(desc1.type, desc2.type) + + def test_as_sddl_invalid(self): + text = "O:AOG:DAD:(A;;RPWPCCDCLCSWRCWDWOGA;;;S-1-0-0)" + dom = security.dom_sid("S-2-0-0") + desc1 = security.descriptor.from_sddl(text, dom) + self.assertRaises(TypeError, desc1.as_sddl,text) + + + def test_as_sddl_no_domainsid(self): + dom = security.dom_sid("S-2-0-0") + text = "O:AOG:DAD:(A;;RPWPCCDCLCSWRCWDWOGA;;;S-1-0-0)" + desc1 = security.descriptor.from_sddl(text, dom) + desc2 = security.descriptor.from_sddl(desc1.as_sddl(), dom) + self.assertEquals(desc1.group_sid, desc2.group_sid) + self.assertEquals(desc1.owner_sid, desc2.owner_sid) + self.assertEquals(desc1.sacl, desc2.sacl) + self.assertEquals(desc1.type, desc2.type) + + def test_domsid_nodomsid_as_sddl(self): + dom = security.dom_sid("S-2-0-0") + text = "O:AOG:DAD:(A;;RPWPCCDCLCSWRCWDWOGA;;;S-1-0-0)" + desc1 = security.descriptor.from_sddl(text, dom) + self.assertNotEqual(desc1.as_sddl(), desc1.as_sddl(dom)) + + def test_split(self): + dom = security.dom_sid("S-2-0-7") + self.assertEquals((security.dom_sid("S-2-0"), 7), dom.split()) + + +class DomSidTests(samba.tests.TestCase): + + def test_parse_sid(self): + sid = security.dom_sid("S-1-5-21") + self.assertEquals("S-1-5-21", str(sid)) + + def test_sid_equal(self): + sid1 = security.dom_sid("S-1-5-21") + sid2 = security.dom_sid("S-1-5-21") + self.assertEquals(sid1, sid1) + self.assertEquals(sid1, sid2) + + def test_random(self): + sid = security.random_sid() + self.assertTrue(str(sid).startswith("S-1-5-21-")) + + def test_repr(self): + sid = security.random_sid() + self.assertTrue(repr(sid).startswith("dom_sid('S-1-5-21-")) + + +class PrivilegeTests(samba.tests.TestCase): + + def test_privilege_name(self): + self.assertEquals("SeShutdownPrivilege", + security.privilege_name(security.SEC_PRIV_SHUTDOWN)) + + def test_privilege_id(self): + self.assertEquals(security.SEC_PRIV_SHUTDOWN, + security.privilege_id("SeShutdownPrivilege")) + diff --git a/source4/scripting/python/samba/tests/upgrade.py b/source4/scripting/python/samba/tests/upgrade.py index 17ebfa7bf5..16ccbd567e 100644 --- a/source4/scripting/python/samba/tests/upgrade.py +++ b/source4/scripting/python/samba/tests/upgrade.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # Unix SMB/CIFS implementation. # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007 @@ -17,11 +17,13 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # -from samba import Ldb +"""Tests for samba.upgrade.""" + from samba.upgrade import import_wins from samba.tests import LdbTestCase class WinsUpgradeTests(LdbTestCase): + def test_upgrade(self): winsdb = { "FOO#20": (200, ["127.0.0.1", "127.0.0.2"], 0x60) diff --git a/source4/scripting/python/samba/tests/upgradeprovision.py b/source4/scripting/python/samba/tests/upgradeprovision.py new file mode 100644 index 0000000000..b81ee8a8ab --- /dev/null +++ b/source4/scripting/python/samba/tests/upgradeprovision.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python + +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2008 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +"""Tests for samba.upgradeprovision.""" + +import os +from samba.upgradehelpers import (usn_in_range, dn_sort, + get_diff_sddls, update_secrets, + construct_existor_expr) + +from samba.tests.provision import create_dummy_secretsdb +from samba.tests import TestCaseInTempDir +from samba import Ldb +from ldb import SCOPE_SUBTREE +import samba.tests + +def dummymessage(a=None, b=None): + pass + + +class UpgradeProvisionTestCase(TestCaseInTempDir): + """Some simple tests for individual functions in the provisioning code. + """ + def test_usn_in_range(self): + range = [5, 25, 35, 55] + + vals = [3, 26, 56] + + for v in vals: + self.assertFalse(usn_in_range(v, range)) + + vals = [5, 20, 25, 35, 36] + + for v in vals: + self.assertTrue(usn_in_range(v, range)) + + def test_dn_sort(self): + # higher level comes after lower even if lexicographicaly closer + # ie dc=tata,dc=toto (2 levels), comes after dc=toto + # even if dc=toto is lexicographicaly after dc=tata, dc=toto + self.assertEquals(dn_sort("dc=tata,dc=toto", "dc=toto"), 1) + self.assertEquals(dn_sort("dc=zata", "dc=tata"), 1) + self.assertEquals(dn_sort("dc=toto,dc=tata", + "cn=foo,dc=toto,dc=tata"), -1) + self.assertEquals(dn_sort("cn=bar, dc=toto,dc=tata", + "cn=foo, dc=toto,dc=tata"), -1) + + def test_get_diff_sddl(self): + sddl = "O:SAG:DUD:AI(A;CIID;RPWPCRCCLCLORCWOWDSW;;;SA)\ +(A;CIID;RP LCLORC;;;AU)(A;CIID;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)S:AI(AU;CIIDSA;WP;;;WD)" + sddl1 = "O:SAG:DUD:AI(A;CIID;RPWPCRCCLCLORCWOWDSW;;;SA)\ +(A;CIID;RP LCLORC;;;AU)(A;CIID;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)S:AI(AU;CIIDSA;WP;;;WD)" + sddl2 = "O:BAG:DUD:AI(A;CIID;RPWPCRCCLCLORCWOWDSW;;;SA)\ +(A;CIID;RP LCLORC;;;AU)(A;CIID;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)S:AI(AU;CIIDSA;WP;;;WD)" + sddl3 = "O:SAG:BAD:AI(A;CIID;RPWPCRCCLCLORCWOWDSW;;;SA)\ +(A;CIID;RP LCLORC;;;AU)(A;CIID;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)S:AI(AU;CIIDSA;WP;;;WD)" + sddl4 = "O:SAG:DUD:AI(A;CIID;RPWPCRCCLCLORCWOWDSW;;;BA)\ +(A;CIID;RP LCLORC;;;AU)(A;CIID;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)S:AI(AU;CIIDSA;WP;;;WD)" + sddl5 = "O:SAG:DUD:AI(A;CIID;RPWPCRCCLCLORCWOWDSW;;;SA)\ +(A;CIID;RP LCLORC;;;AU)(A;CIID;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)" + + self.assertEquals(get_diff_sddls(sddl, sddl1), "") + txt = get_diff_sddls(sddl, sddl2) + self.assertEquals(txt, "\tOwner mismatch: SA (in ref) BA(in current)\n") + txt = get_diff_sddls(sddl, sddl3) + self.assertEquals(txt, "\tGroup mismatch: DU (in ref) BA(in current)\n") + txt = get_diff_sddls(sddl, sddl4) + txtmsg = "\tPart dacl is different between reference and current here\ + is the detail:\n\t\t(A;CIID;RPWPCRCCLCLORCWOWDSW;;;BA) ACE is not present in\ + the reference\n\t\t(A;CIID;RPWPCRCCLCLORCWOWDSW;;;SA) ACE is not present in\ + the current\n" + self.assertEquals(txt, txtmsg) + txt = get_diff_sddls(sddl, sddl5) + self.assertEquals(txt, "\tCurrent ACL hasn't a sacl part\n") + + def test_construct_existor_expr(self): + res = construct_existor_expr([]) + self.assertEquals(res, "") + + res = construct_existor_expr(["foo"]) + self.assertEquals(res, "(|(foo=*))") + + res = construct_existor_expr(["foo", "bar"]) + self.assertEquals(res, "(|(foo=*)(bar=*))") + + +class UpdateSecretsTests(samba.tests.TestCaseInTempDir): + + def setUp(self): + super(UpdateSecretsTests, self).setUp() + self.referencedb = create_dummy_secretsdb( + os.path.join(self.tempdir, "ref.ldb")) + + def _getEmptyDb(self): + return Ldb(os.path.join(self.tempdir, "secrets.ldb")) + + def _getCurrentFormatDb(self): + return create_dummy_secretsdb( + os.path.join(self.tempdir, "secrets.ldb")) + + def test_trivial(self): + # Test that updating an already up-to-date secretsdb works fine + self.secretsdb = self._getCurrentFormatDb() + self.assertEquals(None, + update_secrets(self.referencedb, self.secretsdb, dummymessage)) + + def test_update_modules(self): + empty_db = self._getEmptyDb() + update_secrets(self.referencedb, empty_db, dummymessage) + newmodules = empty_db.search( + expression="dn=@MODULES", base="", scope=SCOPE_SUBTREE) + refmodules = self.referencedb.search( + expression="dn=@MODULES", base="", scope=SCOPE_SUBTREE) + self.assertEquals(newmodules.msgs, refmodules.msgs) + + def tearDown(self): + for name in ["ref.ldb", "secrets.ldb"]: + path = os.path.join(self.tempdir, name) + if os.path.exists(path): + os.unlink(path) + super(UpdateSecretsTests, self).tearDown() + + diff --git a/source4/scripting/python/samba/tests/upgradeprovisionneeddc.py b/source4/scripting/python/samba/tests/upgradeprovisionneeddc.py new file mode 100644 index 0000000000..3a9c78e0dc --- /dev/null +++ b/source4/scripting/python/samba/tests/upgradeprovisionneeddc.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python + +# Unix SMB/CIFS implementation. +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2008 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +"""Tests for samba.upgradeprovision that need a DC.""" + +import os +import re +import shutil + +from samba import param +from samba.credentials import Credentials +from samba.auth import system_session +from samba.provision import getpolicypath +from samba.upgradehelpers import (get_paths, get_ldbs, + find_provision_key_parameters, identic_rename, + updateOEMInfo, getOEMInfo, update_gpo, + delta_update_basesamdb, + update_dns_account_password, + search_constructed_attrs_stored, + increment_calculated_keyversion_number) +from samba.tests import env_loadparm, TestCaseInTempDir +from samba.tests.provision import create_dummy_secretsdb +import ldb + + +def dummymessage(a=None, b=None): + pass + +smb_conf_path = "%s/%s/%s" % (os.environ["SELFTEST_PREFIX"], "dc", "etc/smb.conf") + +class UpgradeProvisionBasicLdbHelpersTestCase(TestCaseInTempDir): + """Some simple tests for individual functions in the provisioning code. + """ + + def test_get_ldbs(self): + paths = get_paths(param, None, smb_conf_path) + creds = Credentials() + lp = env_loadparm() + creds.guess(lp) + get_ldbs(paths, creds, system_session(), lp) + + def test_find_key_param(self): + paths = get_paths(param, None, smb_conf_path) + creds = Credentials() + lp = env_loadparm() + creds.guess(lp) + rootdn = "dc=samba,dc=example,dc=com" + ldbs = get_ldbs(paths, creds, system_session(), lp) + names = find_provision_key_parameters(ldbs.sam, ldbs.secrets, ldbs.idmap, + paths, smb_conf_path, lp) + self.assertEquals(names.realm, "SAMBA.EXAMPLE.COM") + self.assertEquals(str(names.rootdn).lower(), rootdn.lower()) + self.assertNotEquals(names.policyid_dc, None) + self.assertNotEquals(names.ntdsguid, "") + + +class UpgradeProvisionWithLdbTestCase(TestCaseInTempDir): + + def _getEmptyDbName(self): + return os.path.join(self.tempdir, "sam.ldb") + + def setUp(self): + super(UpgradeProvisionWithLdbTestCase, self).setUp() + paths = get_paths(param, None, smb_conf_path) + self.creds = Credentials() + self.lp = env_loadparm() + self.creds.guess(self.lp) + self.paths = paths + self.ldbs = get_ldbs(paths, self.creds, system_session(), self.lp) + self.names = find_provision_key_parameters(self.ldbs.sam, + self.ldbs.secrets, self.ldbs.idmap, paths, smb_conf_path, + self.lp) + self.referencedb = create_dummy_secretsdb( + os.path.join(self.tempdir, "ref.ldb")) + + def test_search_constructed_attrs_stored(self): + hashAtt = search_constructed_attrs_stored(self.ldbs.sam, + self.names.rootdn, + ["msds-KeyVersionNumber"]) + self.assertFalse(hashAtt.has_key("msds-KeyVersionNumber")) + + def test_increment_calculated_keyversion_number(self): + dn = "CN=Administrator,CN=Users,%s" % self.names.rootdn + # We conctruct a simple hash for the user administrator + hash = {} + # And we want the version to be 140 + hash[dn.lower()] = 140 + + increment_calculated_keyversion_number(self.ldbs.sam, + self.names.rootdn, + hash) + self.assertEqual(self.ldbs.sam.get_attribute_replmetadata_version(dn, + "unicodePwd"), + 140) + # This function should not decrement the version + hash[dn.lower()] = 130 + + increment_calculated_keyversion_number(self.ldbs.sam, + self.names.rootdn, + hash) + self.assertEqual(self.ldbs.sam.get_attribute_replmetadata_version(dn, + "unicodePwd"), + 140) + + def test_identic_rename(self): + rootdn = "DC=samba,DC=example,DC=com" + + guestDN = ldb.Dn(self.ldbs.sam, "CN=Guest,CN=Users,%s" % rootdn) + identic_rename(self.ldbs.sam, guestDN) + res = self.ldbs.sam.search(expression="(name=Guest)", base=rootdn, + scope=ldb.SCOPE_SUBTREE, attrs=["dn"]) + self.assertEquals(len(res), 1) + self.assertEquals(str(res[0]["dn"]), "CN=Guest,CN=Users,%s" % rootdn) + + def test_delta_update_basesamdb(self): + dummysampath = self._getEmptyDbName() + delta_update_basesamdb(self.paths.samdb, dummysampath, + self.creds, system_session(), self.lp, + dummymessage) + + def test_update_gpo_simple(self): + dir = getpolicypath(self.paths.sysvol, self.names.dnsdomain, + self.names.policyid) + shutil.rmtree(dir) + self.assertFalse(os.path.isdir(dir)) + update_gpo(self.paths, self.ldbs.sam, self.names, self.lp, dummymessage) + self.assertTrue(os.path.isdir(dir)) + + def test_update_gpo_acl(self): + path = os.path.join(self.tempdir, "testupdategpo") + save = self.paths.sysvol + self.paths.sysvol = path + os.mkdir(path) + os.mkdir(os.path.join(path, self.names.dnsdomain)) + os.mkdir(os.path.join(os.path.join(path, self.names.dnsdomain), + "Policies")) + update_gpo(self.paths, self.ldbs.sam, self.names, self.lp, dummymessage) + shutil.rmtree(path) + self.paths.sysvol = save + + def test_getOEMInfo(self): + realm = self.lp.get("realm") + basedn = "DC=%s" % realm.replace(".", ", DC=") + oem = getOEMInfo(self.ldbs.sam, basedn) + self.assertNotEquals(oem, "") + + def test_update_dns_account(self): + update_dns_account_password(self.ldbs.sam, self.ldbs.secrets, self.names) + + def test_updateOEMInfo(self): + realm = self.lp.get("realm") + basedn = "DC=%s" % realm.replace(".", ", DC=") + oem = getOEMInfo(self.ldbs.sam, basedn) + updateOEMInfo(self.ldbs.sam, basedn) + oem2 = getOEMInfo(self.ldbs.sam, basedn) + self.assertNotEquals(str(oem), str(oem2)) + self.assertTrue(re.match(".*upgrade to.*", str(oem2))) + + def tearDown(self): + for name in ["ref.ldb", "secrets.ldb", "sam.ldb"]: + path = os.path.join(self.tempdir, name) + if os.path.exists(path): + os.unlink(path) + super(UpgradeProvisionWithLdbTestCase, self).tearDown() diff --git a/source4/scripting/python/samba/tests/xattr.py b/source4/scripting/python/samba/tests/xattr.py new file mode 100644 index 0000000000..f978ee5b2a --- /dev/null +++ b/source4/scripting/python/samba/tests/xattr.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python + +# Unix SMB/CIFS implementation. Tests for xattr manipulation +# Copyright (C) Matthieu Patou <mat@matws.net> 2009 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +"""Tests for samba.xattr_native and samba.xattr_tdb.""" + +import samba.xattr_native, samba.xattr_tdb +from samba.dcerpc import xattr +from samba.ndr import ndr_pack +from samba.tests import TestCase, TestSkipped +import random +import os + +class XattrTests(TestCase): + + def _tmpfilename(self): + random.seed() + path = os.environ['SELFTEST_PREFIX'] + return os.path.join(path, "pytests"+str(int(100000*random.random()))) + + def _eadbpath(self): + return os.path.join(os.environ['SELFTEST_PREFIX'], "eadb.tdb") + + def test_set_xattr_native(self): + if not samba.xattr_native.is_xattr_supported(): + raise TestSkipped() + ntacl = xattr.NTACL() + ntacl.version = 1 + tempf = self._tmpfilename() + open(tempf, 'w').write("empty") + try: + samba.xattr_native.wrap_setxattr(tempf, "user.unittests", + ndr_pack(ntacl)) + except IOError: + raise TestSkipped("the filesystem where the tests are runned do not support XATTR") + os.unlink(tempf) + + def test_set_and_get_native(self): + if not samba.xattr_native.is_xattr_supported(): + raise TestSkipped() + tempf = self._tmpfilename() + reftxt = "this is a test" + open(tempf, 'w').write("empty") + try: + samba.xattr_native.wrap_setxattr(tempf, "user.unittests", reftxt) + text = samba.xattr_native.wrap_getxattr(tempf, "user.unittests") + self.assertEquals(text, reftxt) + except IOError: + raise TestSkipped("the filesystem where the tests are runned do not support XATTR") + os.unlink(tempf) + + def test_set_xattr_tdb(self): + tempf = self._tmpfilename() + eadb_path = self._eadbpath() + ntacl = xattr.NTACL() + ntacl.version = 1 + open(tempf, 'w').write("empty") + try: + samba.xattr_tdb.wrap_setxattr(eadb_path, + tempf, "user.unittests", ndr_pack(ntacl)) + finally: + os.unlink(tempf) + os.unlink(eadb_path) + + def test_set_tdb_not_open(self): + tempf = self._tmpfilename() + ntacl = xattr.NTACL() + ntacl.version = 1 + open(tempf, 'w').write("empty") + try: + self.assertRaises(IOError, samba.xattr_tdb.wrap_setxattr, + os.path.join("nonexistent", "eadb.tdb"), tempf, + "user.unittests", ndr_pack(ntacl)) + finally: + os.unlink(tempf) + + def test_set_and_get_tdb(self): + tempf = self._tmpfilename() + eadb_path = self._eadbpath() + reftxt = "this is a test" + open(tempf, 'w').write("empty") + try: + samba.xattr_tdb.wrap_setxattr(eadb_path, tempf, "user.unittests", + reftxt) + text = samba.xattr_tdb.wrap_getxattr(eadb_path, tempf, + "user.unittests") + self.assertEquals(text, reftxt) + finally: + os.unlink(tempf) + os.unlink(eadb_path) diff --git a/source4/scripting/python/samba/torture/pytorture b/source4/scripting/python/samba/torture/pytorture deleted file mode 100755 index e0123447e8..0000000000 --- a/source4/scripting/python/samba/torture/pytorture +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/python - -import sys -from optparse import OptionParser - -# Parse command line - -parser = OptionParser() - -parser.add_option("-b", "--binding", action="store", type="string", - dest="binding") - -parser.add_option("-d", "--domain", action="store", type="string", - dest="domain") - -parser.add_option("-u", "--username", action="store", type="string", - dest="username") - -parser.add_option("-p", "--password", action="store", type="string", - dest="password") - -(options, args) = parser.parse_args() - -if not options.binding: - parser.error('You must supply a binding string') - -if not options.username or not options.password or not options.domain: - parser.error('You must supply a domain, username and password') - -binding = options.binding -domain = options.domain -username = options.username -password = options.password - -if len(args) == 0: - parser.error('You must supply the name of a module to test') - -# Import and test - -for test in args: - - try: - module = __import__('torture_%s' % test) - except ImportError: - print 'No such module "%s"' % test - sys.exit(1) - - if not hasattr(module, 'runtests'): - print 'Module "%s" does not have a runtests function' % test - - module.runtests(binding, (domain, username, password)) diff --git a/source4/scripting/python/samba/torture/spoolss.py b/source4/scripting/python/samba/torture/spoolss.py deleted file mode 100644 index a75385e079..0000000000 --- a/source4/scripting/python/samba/torture/spoolss.py +++ /dev/null @@ -1,437 +0,0 @@ -import sys, string -import dcerpc - - -def ResizeBufferCall(fn, pipe, r): - - r['buffer'] = None - r['buf_size'] = 0 - - result = fn(pipe, r) - - if result['result'] == dcerpc.WERR_INSUFFICIENT_BUFFER or \ - result['result'] == dcerpc.WERR_MORE_DATA: - r['buffer'] = result['buf_size'] * '\x00' - r['buf_size'] = result['buf_size'] - - result = fn(pipe, r) - - return result - - -def test_OpenPrinterEx(pipe, printer): - - print 'spoolss_OpenPrinterEx(%s)' % printer - - printername = '\\\\%s' % dcerpc.dcerpc_server_name(pipe) - - if printer is not None: - printername = printername + '\\%s' % printer - - r = {} - r['printername'] = printername - r['datatype'] = None - r['devmode_ctr'] = {} - r['devmode_ctr']['size'] = 0 - r['devmode_ctr']['devmode'] = None - r['access_mask'] = 0x02000000 - r['level'] = 1 - r['userlevel'] = {} - r['userlevel']['level1'] = {} - r['userlevel']['level1']['size'] = 0 - r['userlevel']['level1']['client'] = None - r['userlevel']['level1']['user'] = None - r['userlevel']['level1']['build'] = 1381 - r['userlevel']['level1']['major'] = 2 - r['userlevel']['level1']['minor'] = 0 - r['userlevel']['level1']['processor'] = 0 - - result = dcerpc.spoolss_OpenPrinterEx(pipe, r) - - return result['handle'] - - -def test_ClosePrinter(pipe, handle): - - r = {} - r['handle'] = handle - - dcerpc.spoolss_ClosePrinter(pipe, r) - - -def test_GetPrinter(pipe, handle): - - r = {} - r['handle'] = handle - - for level in [0, 1, 2, 3, 4, 5, 6, 7]: - - print 'spoolss_GetPrinter(level = %d)' % level - - r['level'] = level - r['buffer'] = None - r['buf_size'] = 0 - - result = ResizeBufferCall(dcerpc.spoolss_GetPrinter, pipe, r) - - -def test_EnumForms(pipe, handle): - - print 'spoolss_EnumForms()' - - r = {} - r['handle'] = handle - r['level'] = 1 - r['buffer'] = None - r['buf_size'] = 0 - - result = ResizeBufferCall(dcerpc.spoolss_EnumForms, pipe, r) - - forms = dcerpc.unmarshall_spoolss_FormInfo_array( - result['buffer'], r['level'], result['count']) - - for form in forms: - - r = {} - r['handle'] = handle - r['formname'] = form['info1']['formname'] - r['level'] = 1 - - result = ResizeBufferCall(dcerpc.spoolss_GetForm, pipe, r) - - -def test_EnumPorts(pipe, handle): - - print 'spoolss_EnumPorts()' - - for level in [1, 2]: - - r = {} - r['handle'] = handle - r['servername'] = None - r['level'] = level - - result = ResizeBufferCall(dcerpc.spoolss_EnumPorts, pipe, r) - - ports = dcerpc.unmarshall_spoolss_PortInfo_array( - result['buffer'], r['level'], result['count']) - - if level == 1: - port_names = map(lambda x: x['info1']['port_name'], ports) - - -def test_DeleteForm(pipe, handle, formname): - - r = {} - r['handle'] = handle - r['formname'] = formname - - dcerpc.spoolss_DeleteForm(pipe, r) - - -def test_GetForm(pipe, handle, formname): - - r = {} - r['handle'] = handle - r['formname'] = formname - r['level'] = 1 - - result = ResizeBufferCall(dcerpc.spoolss_GetForm, pipe, r) - - return result['info']['info1'] - - -def test_SetForm(pipe, handle, form): - - print 'spoolss_SetForm()' - - r = {} - r['handle'] = handle - r['level'] = 1 - r['formname'] = form['info1']['formname'] - r['info'] = form - - dcerpc.spoolss_SetForm(pipe, r) - - newform = test_GetForm(pipe, handle, r['formname']) - - if form['info1'] != newform: - print 'SetForm: mismatch: %s != %s' % \ - (r['info']['info1'], f) - sys.exit(1) - - -def test_AddForm(pipe, handle): - - print 'spoolss_AddForm()' - - formname = '__testform__' - - r = {} - r['handle'] = handle - r['level'] = 1 - r['info'] = {} - r['info']['info1'] = {} - r['info']['info1']['formname'] = formname - r['info']['info1']['flags'] = 0x0002 - r['info']['info1']['width'] = 100 - r['info']['info1']['length'] = 100 - r['info']['info1']['left'] = 0 - r['info']['info1']['top'] = 1000 - r['info']['info1']['right'] = 2000 - r['info']['info1']['bottom'] = 3000 - - try: - result = dcerpc.spoolss_AddForm(pipe, r) - except dcerpc.WERROR, arg: - if arg[0] == dcerpc.WERR_ALREADY_EXISTS: - test_DeleteForm(pipe, handle, formname) - result = dcerpc.spoolss_AddForm(pipe, r) - - f = test_GetForm(pipe, handle, formname) - - if r['info']['info1'] != f: - print 'AddForm: mismatch: %s != %s' % \ - (r['info']['info1'], f) - sys.exit(1) - - r['formname'] = formname - - test_SetForm(pipe, handle, r['info']) - - test_DeleteForm(pipe, handle, formname) - - -def test_EnumJobs(pipe, handle): - - print 'spoolss_EnumJobs()' - - r = {} - r['handle'] = handle - r['firstjob'] = 0 - r['numjobs'] = 0xffffffff - r['level'] = 1 - - result = ResizeBufferCall(dcerpc.spoolss_EnumJobs, pipe, r) - - if result['buffer'] is None: - return - - jobs = dcerpc.unmarshall_spoolss_JobInfo_array( - result['buffer'], r['level'], result['count']) - - for job in jobs: - - s = {} - s['handle'] = handle - s['job_id'] = job['info1']['job_id'] - s['level'] = 1 - - result = ResizeBufferCall(dcerpc.spoolss_GetJob, pipe, s) - - if result['info'] != job: - print 'EnumJobs: mismatch: %s != %s' % (result['info'], job) - sys.exit(1) - - - # TODO: AddJob, DeleteJob, ScheduleJob - - -def test_EnumPrinterData(pipe, handle): - - print 'test_EnumPrinterData()' - - enum_index = 0 - - while 1: - - r = {} - r['handle'] = handle - r['enum_index'] = enum_index - - r['value_offered'] = 0 - r['data_size'] = 0 - - result = dcerpc.spoolss_EnumPrinterData(pipe, r) - - r['value_offered'] = result['value_needed'] - r['data_size'] = result['data_size'] - - result = dcerpc.spoolss_EnumPrinterData(pipe, r) - - if result['result'] == dcerpc.WERR_NO_MORE_ITEMS: - break - - s = {} - s['handle'] = handle - s['value_name'] = result['value_name'] - - result2 = ResizeBufferCall(dcerpc.spoolss_GetPrinterData, pipe, s) - - if result['buffer'][:result2['buf_size']] != result2['buffer']: - print 'EnumPrinterData/GetPrinterData mismatch' - sys.exit(1) - - enum_index += 1 - - -def test_SetPrinterDataEx(pipe, handle): - - valuename = '__printerdataextest__' - data = '12345' - - r = {} - r['handle'] = handle - r['key_name'] = 'DsSpooler' - r['value_name'] = valuename - r['type'] = 3 - r['buffer'] = data - r['buf_size'] = len(data) - - result = dcerpc.spoolss_SetPrinterDataEx(pipe, r) - - -def test_EnumPrinterDataEx(pipe, handle): - - r = {} - r['handle'] = handle - r['key_name'] = 'DsSpooler' - r['buf_size'] = 0 - - result = dcerpc.spoolss_EnumPrinterDataEx(pipe, r) - - if result['result'] == dcerpc.WERR_MORE_DATA: - r['buf_size'] = result['buf_size'] - - result = dcerpc.spoolss_EnumPrinterDataEx(pipe, r) - - # TODO: test spoolss_GetPrinterDataEx() - - -def test_SetPrinterData(pipe, handle): - - print 'testing spoolss_SetPrinterData()' - - valuename = '__printerdatatest__' - data = '12345' - - r = {} - r['handle'] = handle - r['value_name'] = valuename - r['type'] = 3 # REG_BINARY - r['buffer'] = data - r['real_len'] = 5 - - dcerpc.spoolss_SetPrinterData(pipe, r) - - s = {} - s['handle'] = handle - s['value_name'] = valuename - - result = ResizeBufferCall(dcerpc.spoolss_GetPrinterData, pipe, r) - - if result['buffer'] != data: - print 'SetPrinterData: mismatch' - sys.exit(1) - - dcerpc.spoolss_DeletePrinterData(pipe, r) - - -def test_EnumPrinters(pipe): - - print 'testing spoolss_EnumPrinters()' - - printer_names = None - - r = {} - r['flags'] = 0x02 - r['server'] = None - - for level in [0, 1, 2, 4, 5]: - - print 'test_EnumPrinters(level = %d)' % level - - r['level'] = level - - result = ResizeBufferCall(dcerpc.spoolss_EnumPrinters, pipe, r) - - printers = dcerpc.unmarshall_spoolss_PrinterInfo_array( - result['buffer'], r['level'], result['count']) - - if level == 2: - for p in printers: - - # A nice check is for the specversion in the - # devicemode. This has always been observed to be - # 1025. - - if p['info2']['devmode']['specversion'] != 1025: - print 'test_EnumPrinters: specversion != 1025' - sys.exit(1) - - r['level'] = 1 - result = ResizeBufferCall(dcerpc.spoolss_EnumPrinters, pipe, r) - - for printer in dcerpc.unmarshall_spoolss_PrinterInfo_array( - result['buffer'], r['level'], result['count']): - - if string.find(printer['info1']['name'], '\\\\') == 0: - print 'Skipping remote printer %s' % printer['info1']['name'] - continue - - printername = string.split(printer['info1']['name'], ',')[0] - - handle = test_OpenPrinterEx(pipe, printername) - - test_GetPrinter(pipe, handle) - test_EnumPorts(pipe, handle) - test_EnumForms(pipe, handle) - test_AddForm(pipe, handle) - test_EnumJobs(pipe, handle) - test_EnumPrinterData(pipe, handle) - test_EnumPrinterDataEx(pipe, handle) - test_SetPrinterData(pipe, handle) -# test_SetPrinterDataEx(pipe, handle) - test_ClosePrinter(pipe, handle) - - -def test_EnumPrinterDrivers(pipe): - - print 'test spoolss_EnumPrinterDrivers()' - - for level in [1, 2, 3]: - - r = {} - r['server'] = None - r['environment'] = None - r['level'] = level - - result = ResizeBufferCall(dcerpc.spoolss_EnumPrinterDrivers, pipe, r) - - drivers = dcerpc.unmarshall_spoolss_DriverInfo_array( - result['buffer'], r['level'], result['count']) - - if level == 1: - driver_names = map(lambda x: x['info1']['driver_name'], drivers) - - -def test_PrintServer(pipe): - - handle = test_OpenPrinterEx(pipe, None) - - # EnumForms and AddForm tests return WERR_BADFID here (??) - - test_ClosePrinter(pipe, handle) - - -def runtests(binding, domain, username, password): - - print 'Testing SPOOLSS pipe' - - pipe = dcerpc.pipe_connect(binding, - dcerpc.DCERPC_SPOOLSS_UUID, dcerpc.DCERPC_SPOOLSS_VERSION, - domain, username, password) - - test_EnumPrinters(pipe) - test_EnumPrinterDrivers(pipe) - test_PrintServer(pipe) diff --git a/source4/scripting/python/samba/torture/torture_samr.py b/source4/scripting/python/samba/torture/torture_samr.py deleted file mode 100755 index 15c6dc1a76..0000000000 --- a/source4/scripting/python/samba/torture/torture_samr.py +++ /dev/null @@ -1,221 +0,0 @@ -#!/usr/bin/python - -import sys -import dcerpc, samr - -def test_Connect(pipe): - - handle = samr.Connect(pipe) - handle = samr.Connect2(pipe) - handle = samr.Connect3(pipe) - handle = samr.Connect4(pipe) - - # WIN2K3 only? - - try: - handle = samr.Connect5(pipe) - except dcerpc.NTSTATUS, arg: - if arg[0] != 0xc00000d2L: # NT_STATUS_NET_WRITE_FAULT - raise - - return handle - -def test_UserHandle(user_handle): - - # QuerySecurity()/SetSecurity() - - user_handle.SetSecurity(user_handle.QuerySecurity()) - - # GetUserPwInfo() - - user_handle.GetUserPwInfo() - - # GetUserInfo() - - for level in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 20, - 21, 23, 24, 25, 26]: - - try: - user_handle.QueryUserInfo(level) - user_handle.QueryUserInfo2(level) - except dcerpc.NTSTATUS, arg: - if arg[0] != 0xc0000003L: # NT_STATUS_INVALID_INFO_CLASS - raise - - # GetGroupsForUser() - - user_handle.GetGroupsForUser() - - # TestPrivateFunctionsUser() - - try: - user_handle.TestPrivateFunctionsUser() - except dcerpc.NTSTATUS, arg: - if arg[0] != 0xC0000002L: - raise - -def test_GroupHandle(group_handle): - - # QuerySecurity()/SetSecurity() - - group_handle.SetSecurity(group_handle.QuerySecurity()) - - # QueryGroupInfo() - - for level in [1, 2, 3, 4, 5]: - info = group_handle.QueryGroupInfo(level) - - # TODO: SetGroupinfo() - - # QueryGroupMember() - - group_handle.QueryGroupMember() - -def test_AliasHandle(alias_handle): - - # QuerySecurity()/SetSecurity() - - alias_handle.SetSecurity(alias_handle.QuerySecurity()) - - print alias_handle.GetMembersInAlias() - -def test_DomainHandle(name, sid, domain_handle): - - print 'testing %s (%s)' % (name, sid) - - # QuerySecurity()/SetSecurity() - - domain_handle.SetSecurity(domain_handle.QuerySecurity()) - - # LookupNames(), none mapped - - try: - domain_handle.LookupNames(['xxNONAMExx']) - except dcerpc.NTSTATUS, arg: - if arg[0] != 0xc0000073L: - raise dcerpc.NTSTATUS(arg) - - # LookupNames(), some mapped - - if name != 'Builtin': - domain_handle.LookupNames(['Administrator', 'xxNONAMExx']) - - # QueryDomainInfo()/SetDomainInfo() - - levels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13] - set_ok = [1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0] - - for i in range(len(levels)): - - info = domain_handle.QueryDomainInfo(level = levels[i]) - - try: - domain_handle.SetDomainInfo(levels[i], info) - except dcerpc.NTSTATUS, arg: - if not (arg[0] == 0xc0000003L and not set_ok[i]): - raise - - # QueryDomainInfo2() - - levels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13] - - for i in range(len(levels)): - domain_handle.QueryDomainInfo2(level = levels[i]) - - # EnumDomainUsers - - print 'testing users' - - users = domain_handle.EnumDomainUsers() - rids = domain_handle.LookupNames(users) - - for i in range(len(users)): - test_UserHandle(domain_handle.OpenUser(rids[0][i])) - - # QueryDisplayInfo - - for i in [1, 2, 3, 4, 5]: - domain_handle.QueryDisplayInfo(level = i) - domain_handle.QueryDisplayInfo2(level = i) - domain_handle.QueryDisplayInfo3(level = i) - - # EnumDomainGroups - - print 'testing groups' - - groups = domain_handle.EnumDomainGroups() - rids = domain_handle.LookupNames(groups) - - for i in range(len(groups)): - test_GroupHandle(domain_handle.OpenGroup(rids[0][i])) - - # EnumDomainAliases - - print 'testing aliases' - - aliases = domain_handle.EnumDomainAliases() - rids = domain_handle.LookupNames(aliases) - - for i in range(len(aliases)): - test_AliasHandle(domain_handle.OpenAlias(rids[0][i])) - - # CreateUser - # CreateUser2 - # CreateDomAlias - # RidToSid - # RemoveMemberFromForeignDomain - # CreateDomainGroup - # GetAliasMembership - - # GetBootKeyInformation() - - try: - domain_handle.GetBootKeyInformation() - except dcerpc.NTSTATUS, arg: - pass - - # TestPrivateFunctionsDomain() - - try: - domain_handle.TestPrivateFunctionsDomain() - except dcerpc.NTSTATUS, arg: - if arg[0] != 0xC0000002L: - raise - -def test_ConnectHandle(connect_handle): - - print 'testing connect handle' - - # QuerySecurity/SetSecurity - - connect_handle.SetSecurity(connect_handle.QuerySecurity()) - - # Lookup bogus domain - - try: - connect_handle.LookupDomain('xxNODOMAINxx') - except dcerpc.NTSTATUS, arg: - if arg[0] != 0xC00000DFL: # NT_STATUS_NO_SUCH_DOMAIN - raise - - # Test all domains - - for domain_name in connect_handle.EnumDomains(): - - connect_handle.GetDomPwInfo(domain_name) - sid = connect_handle.LookupDomain(domain_name) - domain_handle = connect_handle.OpenDomain(sid) - - test_DomainHandle(domain_name, sid, domain_handle) - - # TODO: Test Shutdown() function - -def runtests(binding, creds): - - print 'Testing SAMR pipe' - - pipe = dcerpc.pipe_connect(binding, - dcerpc.DCERPC_SAMR_UUID, int(dcerpc.DCERPC_SAMR_VERSION), creds) - - handle = test_Connect(pipe) - test_ConnectHandle(handle) diff --git a/source4/scripting/python/samba/torture/torture_tdb.py b/source4/scripting/python/samba/torture/torture_tdb.py deleted file mode 100755 index 7f97caf6cb..0000000000 --- a/source4/scripting/python/samba/torture/torture_tdb.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/python - -import sys, os -import Tdb - -def fail(msg): - print 'FAILED:', msg - sys.exit(1) - -tdb_file = '/tmp/torture_tdb.tdb' - -# Create temporary tdb file - -t = Tdb.Tdb(tdb_file, flags = Tdb.CLEAR_IF_FIRST) - -# Check non-existent key throws KeyError exception - -try: - t['__none__'] -except KeyError: - pass -else: - fail('non-existent key did not throw KeyError') - -# Check storing key - -t['bar'] = '1234' -if t['bar'] != '1234': - fail('store key failed') - -# Check key exists - -if not t.has_key('bar'): - fail('has_key() failed for existing key') - -if t.has_key('__none__'): - fail('has_key() succeeded for non-existent key') - -# Delete key - -try: - del(t['__none__']) -except KeyError: - pass -else: - fail('delete of non-existent key did not throw KeyError') - -del t['bar'] -if t.has_key('bar'): - fail('delete of existing key did not delete key') - -# Clear all keys - -t.clear() -if len(t) != 0: - fail('clear failed to remove all keys') - -# Other dict functions - -t['a'] = '1' -t['ab'] = '12' -t['abc'] = '123' - -if len(t) != 3: - fail('len method produced wrong value') - -keys = t.keys() -values = t.values() -items = t.items() - -if set(keys) != set(['a', 'ab', 'abc']): - fail('keys method produced wrong values') - -if set(values) != set(['1', '12', '123']): - fail('values method produced wrong values') - -if set(items) != set([('a', '1'), ('ab', '12'), ('abc', '123')]): - fail('values method produced wrong values') - -t.close() - -# Re-open read-only - -t = Tdb.Tdb(tdb_file, open_flags = os.O_RDONLY) -t.keys() -t.close() - -# Clean up - -os.unlink(tdb_file) diff --git a/source4/scripting/python/samba/torture/winreg.py b/source4/scripting/python/samba/torture/winreg.py deleted file mode 100755 index eb60b9847e..0000000000 --- a/source4/scripting/python/samba/torture/winreg.py +++ /dev/null @@ -1,165 +0,0 @@ -#!/usr/bin/python - -import sys, dcerpc - -def test_OpenHKLM(pipe): - - r = {} - r['unknown'] = {} - r['unknown']['unknown0'] = 0x9038 - r['unknown']['unknown1'] = 0x0000 - r['access_required'] = 0x02000000 - - result = dcerpc.winreg_OpenHKLM(pipe, r) - - return result['handle'] - -def test_QueryInfoKey(pipe, handle): - - r = {} - r['handle'] = handle - r['class'] = {} - r['class']['name'] = None - - return dcerpc.winreg_QueryInfoKey(pipe, r) - -def test_CloseKey(pipe, handle): - - r = {} - r['handle'] = handle - - dcerpc.winreg_CloseKey(pipe, r) - -def test_FlushKey(pipe, handle): - - r = {} - r['handle'] = handle - - dcerpc.winreg_FlushKey(pipe, r) - -def test_GetVersion(pipe, handle): - - r = {} - r['handle'] = handle - - dcerpc.winreg_GetVersion(pipe, r) - -def test_GetKeySecurity(pipe, handle): - - r = {} - r['handle'] = handle - r['unknown'] = 4 - r['size'] = None - r['data'] = {} - r['data']['max_len'] = 0 - r['data']['data'] = '' - - result = dcerpc.winreg_GetKeySecurity(pipe, r) - - print result - - if result['result'] == dcerpc.WERR_INSUFFICIENT_BUFFER: - r['size'] = {} - r['size']['max_len'] = result['data']['max_len'] - r['size']['offset'] = 0 - r['size']['len'] = result['data']['max_len'] - - result = dcerpc.winreg_GetKeySecurity(pipe, r) - - print result - - sys.exit(1) - -def test_Key(pipe, handle, name, depth = 0): - - # Don't descend too far. Registries can be very deep. - - if depth > 2: - return - - try: - keyinfo = test_QueryInfoKey(pipe, handle) - except dcerpc.WERROR, arg: - if arg[0] == dcerpc.WERR_ACCESS_DENIED: - return - - test_GetVersion(pipe, handle) - - test_FlushKey(pipe, handle) - - test_GetKeySecurity(pipe, handle) - - # Enumerate values in this key - - r = {} - r['handle'] = handle - r['name_in'] = {} - r['name_in']['len'] = 0 - r['name_in']['max_len'] = (keyinfo['max_valnamelen'] + 1) * 2 - r['name_in']['buffer'] = {} - r['name_in']['buffer']['max_len'] = keyinfo['max_valnamelen'] + 1 - r['name_in']['buffer']['offset'] = 0 - r['name_in']['buffer']['len'] = 0 - r['type'] = 0 - r['value_in'] = {} - r['value_in']['max_len'] = keyinfo['max_valbufsize'] - r['value_in']['offset'] = 0 - r['value_in']['len'] = 0 - r['value_len1'] = keyinfo['max_valbufsize'] - r['value_len2'] = 0 - - for i in range(0, keyinfo['num_values']): - - r['enum_index'] = i - - dcerpc.winreg_EnumValue(pipe, r) - - # Recursively test subkeys of this key - - r = {} - r['handle'] = handle - r['key_name_len'] = 0 - r['unknown'] = 0x0414 - r['in_name'] = {} - r['in_name']['unknown'] = 0x20a - r['in_name']['key_name'] = {} - r['in_name']['key_name']['name'] = None - r['class'] = {} - r['class']['name'] = None - r['last_changed_time'] = {} - r['last_changed_time']['low'] = 0 - r['last_changed_time']['high'] = 0 - - for i in range(0, keyinfo['num_subkeys']): - - r['enum_index'] = i - - subkey = dcerpc.winreg_EnumKey(pipe, r) - - s = {} - s['handle'] = handle - s['keyname'] = {} - s['keyname']['name'] = subkey['out_name']['name'] - s['unknown'] = 0 - s['access_mask'] = 0x02000000 - - result = dcerpc.winreg_OpenKey(pipe, s) - - test_Key(pipe, result['handle'], name + '/' + s['keyname']['name'], - depth + 1) - - test_CloseKey(pipe, result['handle']) - - # Enumerate values - -def runtests(binding, domain, username, password): - - print 'Testing WINREG pipe' - - pipe = dcerpc.pipe_connect(binding, - dcerpc.DCERPC_WINREG_UUID, dcerpc.DCERPC_WINREG_VERSION, - domain, username, password) - - handle = test_OpenHKLM(pipe) - - test_Key(pipe, handle, 'HKLM') diff --git a/source4/scripting/python/samba/upgrade.py b/source4/scripting/python/samba/upgrade.py index 81945525e6..5bcc4294ba 100644 --- a/source4/scripting/python/samba/upgrade.py +++ b/source4/scripting/python/samba/upgrade.py @@ -1,22 +1,32 @@ -#!/usr/bin/python +# backend code for upgrading from Samba3 +# Copyright Jelmer Vernooij 2005-2007 # -# backend code for upgrading from Samba3 -# Copyright Jelmer Vernooij 2005-2007 -# Released under the GNU GPL v3 or later +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # """Support code for upgrading from Samba 3 to Samba 4.""" __docformat__ = "restructuredText" -from provision import provision, FILL_DRS import grp import ldb import time import pwd -import registry -from samba import Ldb + +from samba import Ldb, registry from samba.param import LoadParm +from samba.provision import provision def import_sam_policy(samldb, policy, dn): """Import a Samba 3 policy database.""" @@ -34,7 +44,7 @@ samba3UserMustLogonToChangePassword: %d samba3BadLockoutMinutes: %d samba3DisconnectTime: %d -""" % (dn, policy.min_password_length, +""" % (dn, policy.min_password_length, policy.password_history, policy.minimum_password_age, policy.maximum_password_age, policy.lockout_duration, policy.reset_count_minutes, policy.user_must_logon_to_change_password, @@ -43,7 +53,7 @@ samba3DisconnectTime: %d def import_sam_account(samldb,acc,domaindn,domainsid): """Import a Samba 3 SAM account. - + :param samldb: Samba 4 SAM Database handle :param acc: Samba 3 account :param domaindn: Domain DN @@ -59,7 +69,7 @@ def import_sam_account(samldb,acc,domaindn,domainsid): if acc.fullname is None: acc.fullname = acc.username - + assert acc.fullname is not None assert acc.nt_username is not None @@ -78,8 +88,8 @@ def import_sam_account(samldb,acc,domaindn,domainsid): "samba3Domain": acc.domain, "samba3DirDrive": acc.dir_drive, "samba3MungedDial": acc.munged_dial, - "samba3Homedir": acc.homedir, - "samba3LogonScript": acc.logon_script, + "samba3Homedir": acc.homedir, + "samba3LogonScript": acc.logon_script, "samba3ProfilePath": acc.profile_path, "samba3Workstations": acc.workstations, "samba3KickOffTime": str(acc.kickoff_time), @@ -95,7 +105,7 @@ def import_sam_account(samldb,acc,domaindn,domainsid): def import_sam_group(samldb, sid, gid, sid_name_use, nt_name, comment, domaindn): """Upgrade a SAM group. - + :param samldb: SAM database. :param gid: Group GID :param sid_name_use: SID name use @@ -109,7 +119,7 @@ def import_sam_group(samldb, sid, gid, sid_name_use, nt_name, comment, domaindn) if nt_name in ("Domain Guests", "Domain Users", "Domain Admins"): return None - + if gid == -1: gr = grp.getgrnam(nt_name) else: @@ -121,12 +131,12 @@ def import_sam_group(samldb, sid, gid, sid_name_use, nt_name, comment, domaindn) unixname = gr.gr_name assert unixname is not None - + samldb.add({ "dn": "cn=%s,%s" % (nt_name, domaindn), "objectClass": ["top", "group"], "description": comment, - "cn": nt_name, + "cn": nt_name, "objectSid": sid, "unixName": unixname, "samba3SidNameUse": str(sid_name_use) @@ -160,7 +170,7 @@ def import_idmap(samdb,samba3_idmap,domaindn): def import_wins(samba4_winsdb, samba3_winsdb): """Import settings from a Samba3 WINS database. - + :param samba4_winsdb: WINS database to import to :param samba3_winsdb: WINS database to import from """ @@ -208,73 +218,6 @@ def import_wins(samba4_winsdb, samba3_winsdb): "objectClass": "winsMaxVersion", "maxVersion": str(version_id)}) -def upgrade_provision(samba3, setup_dir, message, credentials, session_info, smbconf, targetdir): - oldconf = samba3.get_conf() - - if oldconf.get("domain logons") == "True": - serverrole = "domain controller" - else: - if oldconf.get("security") == "user": - serverrole = "standalone" - else: - serverrole = "member server" - - domainname = oldconf.get("workgroup") - if domainname: - domainname = str(domainname) - realm = oldconf.get("realm") - netbiosname = oldconf.get("netbios name") - - secrets_db = samba3.get_secrets_db() - - if domainname is None: - domainname = secrets_db.domains()[0] - message("No domain specified in smb.conf file, assuming '%s'" % domainname) - - if realm is None: - realm = domainname.lower() - message("No realm specified in smb.conf file, assuming '%s'\n" % realm) - - domainguid = secrets_db.get_domain_guid(domainname) - domainsid = secrets_db.get_sid(domainname) - if domainsid is None: - message("Can't find domain secrets for '%s'; using random SID\n" % domainname) - - if netbiosname is not None: - machinepass = secrets_db.get_machine_password(netbiosname) - else: - machinepass = None - - result = provision(setup_dir=setup_dir, message=message, - samdb_fill=FILL_DRS, smbconf=smbconf, session_info=session_info, - credentials=credentials, realm=realm, - domain=domainname, domainsid=domainsid, domainguid=domainguid, - machinepass=machinepass, serverrole=serverrole, targetdir=targetdir) - - import_wins(Ldb(result.paths.winsdb), samba3.get_wins_db()) - - # FIXME: import_registry(registry.Registry(), samba3.get_registry()) - - # FIXME: import_idmap(samdb,samba3.get_idmap_db(),domaindn) - - groupdb = samba3.get_groupmapping_db() - for sid in groupdb.groupsids(): - (gid, sid_name_use, nt_name, comment) = groupdb.get_group(sid) - # FIXME: import_sam_group(samdb, sid, gid, sid_name_use, nt_name, comment, domaindn) - - # FIXME: Aliases - - passdb = samba3.get_sam_db() - for name in passdb: - user = passdb[name] - #FIXME: import_sam_account(result.samdb, user, domaindn, domainsid) - - if hasattr(passdb, 'ldap_url'): - message("Enabling Samba3 LDAP mappings for SAM database") - - enable_samba3sam(result.samdb, passdb.ldap_url) - - def enable_samba3sam(samdb, ldapurl): """Enable Samba 3 LDAP URL database. @@ -292,7 +235,7 @@ replace: @LIST smbconf_keep = [ - "dos charset", + "dos charset", "unix charset", "display charset", "comment", @@ -389,7 +332,7 @@ def upgrade_smbconf(oldconf,mark): """Remove configuration variables not present in Samba4 :param oldconf: Old configuration structure - :param mark: Whether removed configuration variables should be + :param mark: Whether removed configuration variables should be kept in the new configuration as "samba3:<name>" """ data = oldconf.data() @@ -434,3 +377,76 @@ def import_registry(samba4_registry, samba3_regdb): key_handle.set_value(value_name, value_type, value_data) +def upgrade_provision(samba3, logger, credentials, session_info, + smbconf, targetdir): + oldconf = samba3.get_conf() + + if oldconf.get("domain logons") == "True": + serverrole = "domain controller" + else: + if oldconf.get("security") == "user": + serverrole = "standalone" + else: + serverrole = "member server" + + domainname = oldconf.get("workgroup") + realm = oldconf.get("realm") + netbiosname = oldconf.get("netbios name") + + secrets_db = samba3.get_secrets_db() + + if domainname is None: + domainname = secrets_db.domains()[0] + logger.warning("No domain specified in smb.conf file, assuming '%s'", + domainname) + + if realm is None: + if oldconf.get("domain logons") == "True": + logger.warning("No realm specified in smb.conf file and being a DC. That upgrade path doesn't work! Please add a 'realm' directive to your old smb.conf to let us know which one you want to use (generally it's the upcased DNS domainname).") + return + else: + realm = domainname.upper() + logger.warning("No realm specified in smb.conf file, assuming '%s'", + realm) + + domainguid = secrets_db.get_domain_guid(domainname) + domainsid = secrets_db.get_sid(domainname) + if domainsid is None: + logger.warning("Can't find domain secrets for '%s'; using random SID", + domainname) + + if netbiosname is not None: + machinepass = secrets_db.get_machine_password(netbiosname) + else: + machinepass = None + + result = provision(logger=logger, + session_info=session_info, credentials=credentials, + targetdir=targetdir, realm=realm, domain=domainname, + domainguid=domainguid, domainsid=domainsid, + hostname=netbiosname, machinepass=machinepass, + serverrole=serverrole) + + import_wins(Ldb(result.paths.winsdb), samba3.get_wins_db()) + + # FIXME: import_registry(registry.Registry(), samba3.get_registry()) + + # FIXME: import_idmap(samdb,samba3.get_idmap_db(),domaindn) + + groupdb = samba3.get_groupmapping_db() + for sid in groupdb.groupsids(): + (gid, sid_name_use, nt_name, comment) = groupdb.get_group(sid) + # FIXME: import_sam_group(samdb, sid, gid, sid_name_use, nt_name, comment, domaindn) + + # FIXME: Aliases + + passdb = samba3.get_sam_db() + for name in passdb: + user = passdb[name] + #FIXME: import_sam_account(result.samdb, user, domaindn, domainsid) + + if hasattr(passdb, 'ldap_url'): + logger.info("Enabling Samba3 LDAP mappings for SAM database") + + enable_samba3sam(result.samdb, passdb.ldap_url) + diff --git a/source4/scripting/python/samba/upgradehelpers.py b/source4/scripting/python/samba/upgradehelpers.py new file mode 100755 index 0000000000..3a7dfb3997 --- /dev/null +++ b/source4/scripting/python/samba/upgradehelpers.py @@ -0,0 +1,953 @@ +#!/usr/bin/env python +# +# Helpers for provision stuff +# Copyright (C) Matthieu Patou <mat@matws.net> 2009-2010 +# +# Based on provision a Samba4 server by +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2008 +# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2008 +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +"""Helers used for upgrading between different database formats.""" + +import os +import string +import re +import shutil +import samba + +from samba import Ldb, version, ntacls +from samba.dsdb import DS_DOMAIN_FUNCTION_2000 +from ldb import SCOPE_SUBTREE, SCOPE_ONELEVEL, SCOPE_BASE +import ldb +from samba.provision import (ProvisionNames, provision_paths_from_lp, + getpolicypath, set_gpos_acl, create_gpo_struct, + FILL_FULL, provision, ProvisioningError, + setsysvolacl, secretsdb_self_join) +from samba.dcerpc import misc, security, xattr +from samba.dcerpc.misc import SEC_CHAN_BDC +from samba.ndr import ndr_unpack +from samba.samdb import SamDB + +# All the ldb related to registry are commented because the path for them is +# relative in the provisionPath object +# And so opening them create a file in the current directory which is not what +# we want +# I still keep them commented because I plan soon to make more cleaner +ERROR = -1 +SIMPLE = 0x00 +CHANGE = 0x01 +CHANGESD = 0x02 +GUESS = 0x04 +PROVISION = 0x08 +CHANGEALL = 0xff + +hashAttrNotCopied = set(["dn", "whenCreated", "whenChanged", "objectGUID", + "uSNCreated", "replPropertyMetaData", "uSNChanged", "parentGUID", + "objectCategory", "distinguishedName", "nTMixedDomain", + "showInAdvancedViewOnly", "instanceType", "msDS-Behavior-Version", + "nextRid", "cn", "versionNumber", "lmPwdHistory", "pwdLastSet", + "ntPwdHistory", "unicodePwd","dBCSPwd", "supplementalCredentials", + "gPCUserExtensionNames", "gPCMachineExtensionNames","maxPwdAge", "secret", + "possibleInferiors", "privilege", "sAMAccountType"]) + + +class ProvisionLDB(object): + + def __init__(self): + self.sam = None + self.secrets = None + self.idmap = None + self.privilege = None + self.hkcr = None + self.hkcu = None + self.hku = None + self.hklm = None + + def startTransactions(self): + self.sam.transaction_start() + self.secrets.transaction_start() + self.idmap.transaction_start() + self.privilege.transaction_start() +# TO BE DONE +# self.hkcr.transaction_start() +# self.hkcu.transaction_start() +# self.hku.transaction_start() +# self.hklm.transaction_start() + + def groupedRollback(self): + ok = True + try: + self.sam.transaction_cancel() + except Exception: + ok = False + + try: + self.secrets.transaction_cancel() + except Exception: + ok = False + + try: + self.idmap.transaction_cancel() + except Exception: + ok = False + + try: + self.privilege.transaction_cancel() + except Exception: + ok = False + + return ok +# TO BE DONE +# self.hkcr.transaction_cancel() +# self.hkcu.transaction_cancel() +# self.hku.transaction_cancel() +# self.hklm.transaction_cancel() + + def groupedCommit(self): + try: + self.sam.transaction_prepare_commit() + self.secrets.transaction_prepare_commit() + self.idmap.transaction_prepare_commit() + self.privilege.transaction_prepare_commit() + except Exception: + return self.groupedRollback() +# TO BE DONE +# self.hkcr.transaction_prepare_commit() +# self.hkcu.transaction_prepare_commit() +# self.hku.transaction_prepare_commit() +# self.hklm.transaction_prepare_commit() + try: + self.sam.transaction_commit() + self.secrets.transaction_commit() + self.idmap.transaction_commit() + self.privilege.transaction_commit() + except Exception: + return self.groupedRollback() + +# TO BE DONE +# self.hkcr.transaction_commit() +# self.hkcu.transaction_commit() +# self.hku.transaction_commit() +# self.hklm.transaction_commit() + return True + +def get_ldbs(paths, creds, session, lp): + """Return LDB object mapped on most important databases + + :param paths: An object holding the different importants paths for provision object + :param creds: Credential used for openning LDB files + :param session: Session to use for openning LDB files + :param lp: A loadparam object + :return: A ProvisionLDB object that contains LDB object for the different LDB files of the provision""" + + ldbs = ProvisionLDB() + + ldbs.sam = SamDB(paths.samdb, session_info=session, credentials=creds, lp=lp, options=["modules:samba_dsdb"]) + ldbs.secrets = Ldb(paths.secrets, session_info=session, credentials=creds, lp=lp) + ldbs.idmap = Ldb(paths.idmapdb, session_info=session, credentials=creds, lp=lp) + ldbs.privilege = Ldb(paths.privilege, session_info=session, credentials=creds, lp=lp) +# ldbs.hkcr = Ldb(paths.hkcr, session_info=session, credentials=creds, lp=lp) +# ldbs.hkcu = Ldb(paths.hkcu, session_info=session, credentials=creds, lp=lp) +# ldbs.hku = Ldb(paths.hku, session_info=session, credentials=creds, lp=lp) +# ldbs.hklm = Ldb(paths.hklm, session_info=session, credentials=creds, lp=lp) + + return ldbs + + +def usn_in_range(usn, range): + """Check if the usn is in one of the range provided. + To do so, the value is checked to be between the lower bound and + higher bound of a range + + :param usn: A integer value corresponding to the usn that we want to update + :param range: A list of integer representing ranges, lower bounds are in + the even indices, higher in odd indices + :return: True if the usn is in one of the range, False otherwise + """ + + idx = 0 + cont = True + ok = False + while cont: + if idx == len(range): + cont = False + continue + if usn < int(range[idx]): + if idx %2 == 1: + ok = True + cont = False + if usn == int(range[idx]): + cont = False + ok = True + idx = idx + 1 + return ok + + +def get_paths(param, targetdir=None, smbconf=None): + """Get paths to important provision objects (smb.conf, ldb files, ...) + + :param param: Param object + :param targetdir: Directory where the provision is (or will be) stored + :param smbconf: Path to the smb.conf file + :return: A list with the path of important provision objects""" + if targetdir is not None: + etcdir = os.path.join(targetdir, "etc") + if not os.path.exists(etcdir): + os.makedirs(etcdir) + smbconf = os.path.join(etcdir, "smb.conf") + if smbconf is None: + smbconf = param.default_path() + + if not os.path.exists(smbconf): + raise ProvisioningError("Unable to find smb.conf") + + lp = param.LoadParm() + lp.load(smbconf) + paths = provision_paths_from_lp(lp, lp.get("realm")) + return paths + +def update_policyids(names, samdb): + """Update policy ids that could have changed after sam update + + :param names: List of key provision parameters + :param samdb: An Ldb object conntected with the sam DB + """ + # policy guid + res = samdb.search(expression="(displayName=Default Domain Policy)", + base="CN=Policies,CN=System," + str(names.rootdn), + scope=SCOPE_ONELEVEL, attrs=["cn","displayName"]) + names.policyid = str(res[0]["cn"]).replace("{","").replace("}","") + # dc policy guid + res2 = samdb.search(expression="(displayName=Default Domain Controllers" + " Policy)", + base="CN=Policies,CN=System," + str(names.rootdn), + scope=SCOPE_ONELEVEL, attrs=["cn","displayName"]) + if len(res2) == 1: + names.policyid_dc = str(res2[0]["cn"]).replace("{","").replace("}","") + else: + names.policyid_dc = None + + +def find_provision_key_parameters(samdb, secretsdb, idmapdb, paths, smbconf, lp): + """Get key provision parameters (realm, domain, ...) from a given provision + + :param samdb: An LDB object connected to the sam.ldb file + :param secretsdb: An LDB object connected to the secrets.ldb file + :param idmapdb: An LDB object connected to the idmap.ldb file + :param paths: A list of path to provision object + :param smbconf: Path to the smb.conf file + :param lp: A LoadParm object + :return: A list of key provision parameters + """ + names = ProvisionNames() + names.adminpass = None + + # NT domain, kerberos realm, root dn, domain dn, domain dns name + names.domain = string.upper(lp.get("workgroup")) + names.realm = lp.get("realm") + basedn = "DC=" + names.realm.replace(".",",DC=") + names.dnsdomain = names.realm.lower() + names.realm = string.upper(names.realm) + # netbiosname + # Get the netbiosname first (could be obtained from smb.conf in theory) + res = secretsdb.search(expression="(flatname=%s)" % + names.domain,base="CN=Primary Domains", + scope=SCOPE_SUBTREE, attrs=["sAMAccountName"]) + names.netbiosname = str(res[0]["sAMAccountName"]).replace("$","") + + names.smbconf = smbconf + + # That's a bit simplistic but it's ok as long as we have only 3 + # partitions + current = samdb.search(expression="(objectClass=*)", + base="", scope=SCOPE_BASE, + attrs=["defaultNamingContext", "schemaNamingContext", + "configurationNamingContext","rootDomainNamingContext"]) + + names.configdn = current[0]["configurationNamingContext"] + configdn = str(names.configdn) + names.schemadn = current[0]["schemaNamingContext"] + if not (ldb.Dn(samdb, basedn) == (ldb.Dn(samdb, + current[0]["defaultNamingContext"][0]))): + raise ProvisioningError(("basedn in %s (%s) and from %s (%s)" + "is not the same ..." % (paths.samdb, + str(current[0]["defaultNamingContext"][0]), + paths.smbconf, basedn))) + + names.domaindn=current[0]["defaultNamingContext"] + names.rootdn=current[0]["rootDomainNamingContext"] + # default site name + res3 = samdb.search(expression="(objectClass=*)", + base="CN=Sites," + configdn, scope=SCOPE_ONELEVEL, attrs=["cn"]) + names.sitename = str(res3[0]["cn"]) + + # dns hostname and server dn + res4 = samdb.search(expression="(CN=%s)" % names.netbiosname, + base="OU=Domain Controllers,%s" % basedn, + scope=SCOPE_ONELEVEL, attrs=["dNSHostName"]) + names.hostname = str(res4[0]["dNSHostName"]).replace("." + names.dnsdomain,"") + + server_res = samdb.search(expression="serverReference=%s" % res4[0].dn, + attrs=[], base=configdn) + names.serverdn = server_res[0].dn + + # invocation id/objectguid + res5 = samdb.search(expression="(objectClass=*)", + base="CN=NTDS Settings,%s" % str(names.serverdn), scope=SCOPE_BASE, + attrs=["invocationID", "objectGUID"]) + names.invocation = str(ndr_unpack(misc.GUID, res5[0]["invocationId"][0])) + names.ntdsguid = str(ndr_unpack(misc.GUID, res5[0]["objectGUID"][0])) + + # domain guid/sid + res6 = samdb.search(expression="(objectClass=*)", base=basedn, + scope=SCOPE_BASE, attrs=["objectGUID", + "objectSid","msDS-Behavior-Version" ]) + names.domainguid = str(ndr_unpack(misc.GUID, res6[0]["objectGUID"][0])) + names.domainsid = ndr_unpack( security.dom_sid, res6[0]["objectSid"][0]) + if res6[0].get("msDS-Behavior-Version") is None or \ + int(res6[0]["msDS-Behavior-Version"][0]) < DS_DOMAIN_FUNCTION_2000: + names.domainlevel = DS_DOMAIN_FUNCTION_2000 + else: + names.domainlevel = int(res6[0]["msDS-Behavior-Version"][0]) + + # policy guid + res7 = samdb.search(expression="(displayName=Default Domain Policy)", + base="CN=Policies,CN=System," + basedn, + scope=SCOPE_ONELEVEL, attrs=["cn","displayName"]) + names.policyid = str(res7[0]["cn"]).replace("{","").replace("}","") + # dc policy guid + res8 = samdb.search(expression="(displayName=Default Domain Controllers" + " Policy)", + base="CN=Policies,CN=System," + basedn, + scope=SCOPE_ONELEVEL, attrs=["cn","displayName"]) + if len(res8) == 1: + names.policyid_dc = str(res8[0]["cn"]).replace("{","").replace("}","") + else: + names.policyid_dc = None + res9 = idmapdb.search(expression="(cn=%s)" % + (security.SID_BUILTIN_ADMINISTRATORS), + attrs=["xidNumber"]) + if len(res9) == 1: + names.wheel_gid = res9[0]["xidNumber"] + else: + raise ProvisioningError("Unable to find uid/gid for Domain Admins rid") + return names + + +def newprovision(names, creds, session, smbconf, provdir, logger): + """Create a new provision. + + This provision will be the reference for knowing what has changed in the + since the latest upgrade in the current provision + + :param names: List of provision parameters + :param creds: Credentials for the authentification + :param session: Session object + :param smbconf: Path to the smb.conf file + :param provdir: Directory where the provision will be stored + :param logger: A Logger + """ + if os.path.isdir(provdir): + shutil.rmtree(provdir) + os.mkdir(provdir) + logger.info("Provision stored in %s", provdir) + provision(logger, session, creds, smbconf=smbconf, + targetdir=provdir, samdb_fill=FILL_FULL, realm=names.realm, + domain=names.domain, domainguid=names.domainguid, + domainsid=str(names.domainsid), ntdsguid=names.ntdsguid, + policyguid=names.policyid, policyguid_dc=names.policyid_dc, + hostname=names.netbiosname.lower(), hostip=None, hostip6=None, + invocationid=names.invocation, adminpass=names.adminpass, + krbtgtpass=None, machinepass=None, dnspass=None, root=None, + nobody=None, wheel=None, users=None, + serverrole="domain controller", ldap_backend_extra_port=None, + backend_type=None, ldapadminpass=None, ol_mmr_urls=None, + slapd_path=None, setup_ds_path=None, nosync=None, + dom_for_fun_level=names.domainlevel, + ldap_dryrun_mode=None, useeadb=True) + + +def dn_sort(x, y): + """Sorts two DNs in the lexicographical order it and put higher level DN + before. + + So given the dns cn=bar,cn=foo and cn=foo the later will be return as + smaller + + :param x: First object to compare + :param y: Second object to compare + """ + p = re.compile(r'(?<!\\), ?') + tab1 = p.split(str(x)) + tab2 = p.split(str(y)) + minimum = min(len(tab1), len(tab2)) + len1 = len(tab1)-1 + len2 = len(tab2)-1 + # Note: python range go up to upper limit but do not include it + for i in range(0, minimum): + ret = cmp(tab1[len1-i], tab2[len2-i]) + if ret != 0: + return ret + else: + if i == minimum-1: + assert len1!=len2,"PB PB PB" + " ".join(tab1)+" / " + " ".join(tab2) + if len1 > len2: + return 1 + else: + return -1 + return ret + + +def identic_rename(ldbobj, dn): + """Perform a back and forth rename to trigger renaming on attribute that + can't be directly modified. + + :param lbdobj: An Ldb Object + :param dn: DN of the object to manipulate + """ + (before, after) = str(dn).split('=', 1) + # we need to use relax to avoid the subtree_rename constraints + ldbobj.rename(dn, ldb.Dn(ldbobj, "%s=foo%s" % (before, after)), ["relax:0"]) + ldbobj.rename(ldb.Dn(ldbobj, "%s=foo%s" % (before, after)), dn, ["relax:0"]) + + +def chunck_acl(acl): + """Return separate ACE of an ACL + + :param acl: A string representing the ACL + :return: A hash with different parts + """ + + p = re.compile(r'(\w+)?(\(.*?\))') + tab = p.findall(acl) + + hash = {} + hash["aces"] = [] + for e in tab: + if len(e[0]) > 0: + hash["flags"] = e[0] + hash["aces"].append(e[1]) + + return hash + + +def chunck_sddl(sddl): + """ Return separate parts of the SDDL (owner, group, ...) + + :param sddl: An string containing the SDDL to chunk + :return: A hash with the different chunk + """ + + p = re.compile(r'([OGDS]:)(.*?)(?=(?:[GDS]:|$))') + tab = p.findall(sddl) + + hash = {} + for e in tab: + if e[0] == "O:": + hash["owner"] = e[1] + if e[0] == "G:": + hash["group"] = e[1] + if e[0] == "D:": + hash["dacl"] = e[1] + if e[0] == "S:": + hash["sacl"] = e[1] + + return hash + + +def get_diff_sddls(refsddl, cursddl): + """Get the difference between 2 sddl + + This function split the textual representation of ACL into smaller + chunck in order to not to report a simple permutation as a difference + + :param refsddl: First sddl to compare + :param cursddl: Second sddl to compare + :return: A string that explain difference between sddls + """ + + txt = "" + hash_new = chunck_sddl(cursddl) + hash_ref = chunck_sddl(refsddl) + + if hash_new["owner"] != hash_ref["owner"]: + txt = "\tOwner mismatch: %s (in ref) %s" \ + "(in current)\n" % (hash_ref["owner"], hash_new["owner"]) + + if hash_new["group"] != hash_ref["group"]: + txt = "%s\tGroup mismatch: %s (in ref) %s" \ + "(in current)\n" % (txt, hash_ref["group"], hash_new["group"]) + + for part in ["dacl", "sacl"]: + if hash_new.has_key(part) and hash_ref.has_key(part): + + # both are present, check if they contain the same ACE + h_new = set() + h_ref = set() + c_new = chunck_acl(hash_new[part]) + c_ref = chunck_acl(hash_ref[part]) + + for elem in c_new["aces"]: + h_new.add(elem) + + for elem in c_ref["aces"]: + h_ref.add(elem) + + for k in set(h_ref): + if k in h_new: + h_new.remove(k) + h_ref.remove(k) + + if len(h_new) + len(h_ref) > 0: + txt = "%s\tPart %s is different between reference" \ + " and current here is the detail:\n" % (txt, part) + + for item in h_new: + txt = "%s\t\t%s ACE is not present in the" \ + " reference\n" % (txt, item) + + for item in h_ref: + txt = "%s\t\t%s ACE is not present in the" \ + " current\n" % (txt, item) + + elif hash_new.has_key(part) and not hash_ref.has_key(part): + txt = "%s\tReference ACL hasn't a %s part\n" % (txt, part) + elif not hash_new.has_key(part) and hash_ref.has_key(part): + txt = "%s\tCurrent ACL hasn't a %s part\n" % (txt, part) + + return txt + + +def update_secrets(newsecrets_ldb, secrets_ldb, messagefunc): + """Update secrets.ldb + + :param newsecrets_ldb: An LDB object that is connected to the secrets.ldb + of the reference provision + :param secrets_ldb: An LDB object that is connected to the secrets.ldb + of the updated provision + """ + + messagefunc(SIMPLE, "update secrets.ldb") + reference = newsecrets_ldb.search(expression="dn=@MODULES", base="", + scope=SCOPE_SUBTREE) + current = secrets_ldb.search(expression="dn=@MODULES", base="", + scope=SCOPE_SUBTREE) + assert reference, "Reference modules list can not be empty" + if len(current) == 0: + # No modules present + delta = secrets_ldb.msg_diff(ldb.Message(), reference[0]) + delta.dn = reference[0].dn + secrets_ldb.add(reference[0]) + else: + delta = secrets_ldb.msg_diff(current[0], reference[0]) + delta.dn = current[0].dn + secrets_ldb.modify(delta) + + reference = newsecrets_ldb.search(expression="objectClass=top", base="", + scope=SCOPE_SUBTREE, attrs=["dn"]) + current = secrets_ldb.search(expression="objectClass=top", base="", + scope=SCOPE_SUBTREE, attrs=["dn"]) + hash_new = {} + hash = {} + listMissing = [] + listPresent = [] + + empty = ldb.Message() + for i in range(0, len(reference)): + hash_new[str(reference[i]["dn"]).lower()] = reference[i]["dn"] + + # Create a hash for speeding the search of existing object in the + # current provision + for i in range(0, len(current)): + hash[str(current[i]["dn"]).lower()] = current[i]["dn"] + + for k in hash_new.keys(): + if not hash.has_key(k): + listMissing.append(hash_new[k]) + else: + listPresent.append(hash_new[k]) + + for entry in listMissing: + reference = newsecrets_ldb.search(expression="dn=%s" % entry, + base="", scope=SCOPE_SUBTREE) + current = secrets_ldb.search(expression="dn=%s" % entry, + base="", scope=SCOPE_SUBTREE) + delta = secrets_ldb.msg_diff(empty, reference[0]) + for att in hashAttrNotCopied: + delta.remove(att) + messagefunc(CHANGE, "Entry %s is missing from secrets.ldb" % + reference[0].dn) + for att in delta: + messagefunc(CHANGE, " Adding attribute %s" % att) + delta.dn = reference[0].dn + secrets_ldb.add(delta) + + for entry in listPresent: + reference = newsecrets_ldb.search(expression="dn=%s" % entry, + base="", scope=SCOPE_SUBTREE) + current = secrets_ldb.search(expression="dn=%s" % entry, base="", + scope=SCOPE_SUBTREE) + delta = secrets_ldb.msg_diff(current[0], reference[0]) + for att in hashAttrNotCopied: + delta.remove(att) + for att in delta: + if att == "name": + messagefunc(CHANGE, "Found attribute name on %s," + " must rename the DN" % (current[0].dn)) + identic_rename(secrets_ldb, reference[0].dn) + else: + delta.remove(att) + + for entry in listPresent: + reference = newsecrets_ldb.search(expression="dn=%s" % entry, base="", + scope=SCOPE_SUBTREE) + current = secrets_ldb.search(expression="dn=%s" % entry, base="", + scope=SCOPE_SUBTREE) + delta = secrets_ldb.msg_diff(current[0], reference[0]) + for att in hashAttrNotCopied: + delta.remove(att) + for att in delta: + if att == "msDS-KeyVersionNumber": + delta.remove(att) + if att != "dn": + messagefunc(CHANGE, + "Adding/Changing attribute %s to %s" % + (att, current[0].dn)) + + delta.dn = current[0].dn + secrets_ldb.modify(delta) + + res2 = secrets_ldb.search(expression="(samaccountname=dns)", + scope=SCOPE_SUBTREE, attrs=["dn"]) + + if (len(res2) == 1): + messagefunc(SIMPLE, "Remove old dns account") + secrets_ldb.delete(res2[0]["dn"]) + + +def getOEMInfo(samdb, rootdn): + """Return OEM Information on the top level Samba4 use to store version + info in this field + + :param samdb: An LDB object connect to sam.ldb + :param rootdn: Root DN of the domain + :return: The content of the field oEMInformation (if any) + """ + res = samdb.search(expression="(objectClass=*)", base=str(rootdn), + scope=SCOPE_BASE, attrs=["dn", "oEMInformation"]) + if len(res) > 0: + info = res[0]["oEMInformation"] + return info + else: + return "" + + +def updateOEMInfo(samdb, rootdn): + """Update the OEMinfo field to add information about upgrade + + :param samdb: an LDB object connected to the sam DB + :param rootdn: The string representation of the root DN of + the provision (ie. DC=...,DC=...) + """ + res = samdb.search(expression="(objectClass=*)", base=rootdn, + scope=SCOPE_BASE, attrs=["dn", "oEMInformation"]) + if len(res) > 0: + info = res[0]["oEMInformation"] + info = "%s, upgrade to %s" % (info, version) + delta = ldb.Message() + delta.dn = ldb.Dn(samdb, str(res[0]["dn"])) + delta["oEMInformation"] = ldb.MessageElement(info, ldb.FLAG_MOD_REPLACE, + "oEMInformation" ) + samdb.modify(delta) + +def update_gpo(paths, samdb, names, lp, message, force=0): + """Create missing GPO file object if needed + + Set ACL correctly also. + Check ACLs for sysvol/netlogon dirs also + """ + resetacls = False + try: + ntacls.checkset_backend(lp, None, None) + eadbname = lp.get("posix:eadb") + if eadbname is not None and eadbname != "": + try: + attribute = samba.xattr_tdb.wrap_getxattr(eadbname, + paths.sysvol, xattr.XATTR_NTACL_NAME) + except Exception: + attribute = samba.xattr_native.wrap_getxattr(paths.sysvol, + xattr.XATTR_NTACL_NAME) + else: + attribute = samba.xattr_native.wrap_getxattr(paths.sysvol, + xattr.XATTR_NTACL_NAME) + except Exception: + resetacls = True + + if force: + resetacls = True + + dir = getpolicypath(paths.sysvol, names.dnsdomain, names.policyid) + if not os.path.isdir(dir): + create_gpo_struct(dir) + + if names.policyid_dc is None: + raise ProvisioningError("Policy ID for Domain controller is missing") + dir = getpolicypath(paths.sysvol, names.dnsdomain, names.policyid_dc) + if not os.path.isdir(dir): + create_gpo_struct(dir) + # We always reinforce acls on GPO folder because they have to be in sync + # with the one in DS + try: + set_gpos_acl(paths.sysvol, names.dnsdomain, names.domainsid, + names.domaindn, samdb, lp) + except TypeError, e: + message(ERROR, "Unable to set ACLs on policies related objects," + " if not using posix:eadb, you must be root to do it") + + if resetacls: + try: + setsysvolacl(samdb, paths.netlogon, paths.sysvol, names.wheel_gid, + names.domainsid, names.dnsdomain, names.domaindn, lp) + except TypeError, e: + message(ERROR, "Unable to set ACLs on sysvol share, if not using" + "posix:eadb, you must be root to do it") + +def increment_calculated_keyversion_number(samdb, rootdn, hashDns): + """For a given hash associating dn and a number, this function will + update the replPropertyMetaData of each dn in the hash, so that the + calculated value of the msDs-KeyVersionNumber is equal or superior to the + one associated to the given dn. + + :param samdb: An SamDB object pointing to the sam + :param rootdn: The base DN where we want to start + :param hashDns: A hash with dn as key and number representing the + minimum value of msDs-KeyVersionNumber that we want to + have + """ + entry = samdb.search(expression='(objectClass=user)', + base=ldb.Dn(samdb,str(rootdn)), + scope=SCOPE_SUBTREE, attrs=["msDs-KeyVersionNumber"], + controls=["search_options:1:2"]) + done = 0 + hashDone = {} + if len(entry) == 0: + raise ProvisioningError("Unable to find msDs-KeyVersionNumber") + else: + for e in entry: + if hashDns.has_key(str(e.dn).lower()): + val = e.get("msDs-KeyVersionNumber") + if not val: + val = "0" + version = int(str(hashDns[str(e.dn).lower()])) + if int(str(val)) < version: + done = done + 1 + samdb.set_attribute_replmetadata_version(str(e.dn), + "unicodePwd", + version, True) +def delta_update_basesamdb(refsampath, sampath, creds, session, lp, message): + """Update the provision container db: sam.ldb + This function is aimed for alpha9 and newer; + + :param refsampath: Path to the samdb in the reference provision + :param sampath: Path to the samdb in the upgraded provision + :param creds: Credential used for openning LDB files + :param session: Session to use for openning LDB files + :param lp: A loadparam object + :return: A msg_diff object with the difference between the @ATTRIBUTES + of the current provision and the reference provision + """ + + message(SIMPLE, + "Update base samdb by searching difference with reference one") + refsam = Ldb(refsampath, session_info=session, credentials=creds, + lp=lp, options=["modules:"]) + sam = Ldb(sampath, session_info=session, credentials=creds, lp=lp, + options=["modules:"]) + + empty = ldb.Message() + deltaattr = None + reference = refsam.search(expression="") + + for refentry in reference: + entry = sam.search(expression="dn=%s" % refentry["dn"], + scope=SCOPE_SUBTREE) + if not len(entry): + delta = sam.msg_diff(empty, refentry) + message(CHANGE, "Adding %s to sam db" % str(refentry.dn)) + if str(refentry.dn) == "@PROVISION" and\ + delta.get(samba.provision.LAST_PROVISION_USN_ATTRIBUTE): + delta.remove(samba.provision.LAST_PROVISION_USN_ATTRIBUTE) + delta.dn = refentry.dn + sam.add(delta) + else: + delta = sam.msg_diff(entry[0], refentry) + if str(refentry.dn) == "@ATTRIBUTES": + deltaattr = sam.msg_diff(refentry, entry[0]) + if str(refentry.dn) == "@PROVISION" and\ + delta.get(samba.provision.LAST_PROVISION_USN_ATTRIBUTE): + delta.remove(samba.provision.LAST_PROVISION_USN_ATTRIBUTE) + if len(delta.items()) > 1: + delta.dn = refentry.dn + sam.modify(delta) + + return deltaattr + + +def construct_existor_expr(attrs): + """Construct a exists or LDAP search expression. + + :param attrs: List of attribute on which we want to create the search + expression. + :return: A string representing the expression, if attrs is empty an + empty string is returned + """ + expr = "" + if len(attrs) > 0: + expr = "(|" + for att in attrs: + expr = "%s(%s=*)"%(expr,att) + expr = "%s)"%expr + return expr + +def update_machine_account_password(samdb, secrets_ldb, names): + """Update (change) the password of the current DC both in the SAM db and in + secret one + + :param samdb: An LDB object related to the sam.ldb file of a given provision + :param secrets_ldb: An LDB object related to the secrets.ldb file of a given + provision + :param names: List of key provision parameters""" + + expression = "samAccountName=%s$" % names.netbiosname + secrets_msg = secrets_ldb.search(expression=expression, + attrs=["secureChannelType"]) + if int(secrets_msg[0]["secureChannelType"][0]) == SEC_CHAN_BDC: + res = samdb.search(expression=expression, attrs=[]) + assert(len(res) == 1) + + msg = ldb.Message(res[0].dn) + machinepass = samba.generate_random_password(128, 255) + mputf16 = machinepass.encode('utf-16-le') + msg["clearTextPassword"] = ldb.MessageElement(mputf16, + ldb.FLAG_MOD_REPLACE, + "clearTextPassword") + samdb.modify(msg) + + res = samdb.search(expression=("samAccountName=%s$" % names.netbiosname), + attrs=["msDs-keyVersionNumber"]) + assert(len(res) == 1) + kvno = int(str(res[0]["msDs-keyVersionNumber"])) + secChanType = int(secrets_msg[0]["secureChannelType"][0]) + + secretsdb_self_join(secrets_ldb, domain=names.domain, + realm=names.realm, + domainsid=names.domainsid, + dnsdomain=names.dnsdomain, + netbiosname=names.netbiosname, + machinepass=machinepass, + key_version_number=kvno, + secure_channel_type=secChanType) + else: + raise ProvisioningError("Unable to find a Secure Channel" + "of type SEC_CHAN_BDC") + +def update_dns_account_password(samdb, secrets_ldb, names): + """Update (change) the password of the dns both in the SAM db and in + secret one + + :param samdb: An LDB object related to the sam.ldb file of a given provision + :param secrets_ldb: An LDB object related to the secrets.ldb file of a given + provision + :param names: List of key provision parameters""" + + expression = "samAccountName=dns-%s" % names.netbiosname + secrets_msg = secrets_ldb.search(expression=expression) + if len(secrets_msg) == 1: + res = samdb.search(expression=expression, attrs=[]) + assert(len(res) == 1) + + msg = ldb.Message(res[0].dn) + machinepass = samba.generate_random_password(128, 255) + mputf16 = machinepass.encode('utf-16-le') + msg["clearTextPassword"] = ldb.MessageElement(mputf16, + ldb.FLAG_MOD_REPLACE, + "clearTextPassword") + + samdb.modify(msg) + + res = samdb.search(expression=expression, + attrs=["msDs-keyVersionNumber"]) + assert(len(res) == 1) + kvno = str(res[0]["msDs-keyVersionNumber"]) + + msg = ldb.Message(secrets_msg[0].dn) + msg["secret"] = ldb.MessageElement(machinepass, + ldb.FLAG_MOD_REPLACE, + "secret") + msg["msDS-KeyVersionNumber"] = ldb.MessageElement(kvno, + ldb.FLAG_MOD_REPLACE, + "msDS-KeyVersionNumber") + + secrets_ldb.modify(msg) + else: + raise ProvisioningError("Unable to find an object" + " with %s" % expression ) + +def search_constructed_attrs_stored(samdb, rootdn, attrs): + """Search a given sam DB for calculated attributes that are + still stored in the db. + + :param samdb: An LDB object pointing to the sam + :param rootdn: The base DN where the search should start + :param attrs: A list of attributes to be searched + :return: A hash with attributes as key and an array of + array. Each array contains the dn and the associated + values for this attribute as they are stored in the + sam.""" + + hashAtt = {} + expr = construct_existor_expr(attrs) + if expr == "": + return hashAtt + entry = samdb.search(expression=expr, base=ldb.Dn(samdb, str(rootdn)), + scope=SCOPE_SUBTREE, attrs=attrs, + controls=["search_options:1:2","bypassoperational:0"]) + if len(entry) == 0: + # Nothing anymore + return hashAtt + + for ent in entry: + for att in attrs: + if ent.get(att): + if hashAtt.has_key(att): + hashAtt[att][str(ent.dn).lower()] = str(ent[att]) + else: + hashAtt[att] = {} + hashAtt[att][str(ent.dn).lower()] = str(ent[att]) + + return hashAtt + +def int64range2str(value): + """Display the int64 range stored in value as xxx-yyy + + :param value: The int64 range + :return: A string of the representation of the range + """ + + lvalue = long(value) + str = "%d-%d" % (lvalue&0xFFFFFFFF, lvalue>>32) + return str diff --git a/source4/scripting/python/samba/web_server/__init__.py b/source4/scripting/python/samba/web_server/__init__.py new file mode 100644 index 0000000000..da528f42d1 --- /dev/null +++ b/source4/scripting/python/samba/web_server/__init__.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Unix SMB/CIFS implementation. +# Copyright © Jelmer Vernooij <jelmer@samba.org> 2008 +# +# Implementation of SWAT that uses WSGI +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + + + +def render_placeholder(environ, start_response): + status = '200 OK' + response_headers = [('Content-type','text/html')] + start_response(status, response_headers) + + yield "<!doctype html>\n" + yield "<html>\n" + yield " <title>The Samba web service</title>\n" + yield "</html>\n" + + yield "<body>\n" + yield "<p>Welcome to this Samba web server.</p>\n" + yield "<p>This page is a simple placeholder. You probably want to install " + yield "SWAT. More information can be found " + yield "<a href='http://wiki.samba.org/index.php/SWAT'>on the wiki</a>.</p>" + yield "</p>\n" + yield "</body>\n" + yield "</html>\n" + + +__call__ = render_placeholder + + +if __name__ == '__main__': + from wsgiref import simple_server + httpd = simple_server.make_server('localhost', 8090, __call__) + print "Serving HTTP on port 8090..." + httpd.serve_forever() diff --git a/source4/scripting/python/samba_external/README b/source4/scripting/python/samba_external/README new file mode 100644 index 0000000000..d6a4dec7b1 --- /dev/null +++ b/source4/scripting/python/samba_external/README @@ -0,0 +1,4 @@ +This directory is for external python libraries that may not be +installed on the local system. We always should try to use the +system version of the library if possible, then use the Samba +supplied copy if the system copy is unavailable diff --git a/source4/scripting/python/uuidmodule.c b/source4/scripting/python/uuidmodule.c index 98ef9adaa9..3bfe0162ca 100644 --- a/source4/scripting/python/uuidmodule.c +++ b/source4/scripting/python/uuidmodule.c @@ -17,8 +17,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#include "includes.h" #include <Python.h> +#include "includes.h" #include "librpc/ndr/libndr.h" static PyObject *uuid_random(PyObject *self, PyObject *args) diff --git a/source4/scripting/python/wscript_build b/source4/scripting/python/wscript_build new file mode 100644 index 0000000000..540f3b7bb7 --- /dev/null +++ b/source4/scripting/python/wscript_build @@ -0,0 +1,39 @@ +#!/usr/bin/env python + +bld.SAMBA_LIBRARY('samba_python', + source=[], + deps='LIBPYTHON pytalloc-util pyrpc_util', + grouping_library=True, + private_library=True, + pyext=True) + +bld.SAMBA_SUBSYSTEM('LIBPYTHON', + source='modules.c', + public_deps='', + init_function_sentinal='{NULL,NULL}', + deps='talloc', + pyext=True, + ) + + +bld.SAMBA_PYTHON('python_uuid', + source='uuidmodule.c', + deps='ndr', + realname='uuid.so', + enabled = float(bld.env.PYTHON_VERSION) <= 2.4 + ) + + +bld.SAMBA_PYTHON('python_glue', + source='pyglue.c', + deps='pyparam_util samba-util netif pytalloc-util', + realname='samba/_glue.so' + ) + + +# install out various python scripts for use by make test +bld.SAMBA_SCRIPT('samba_python', + pattern='samba/**/*.py', + installdir='python') + +bld.INSTALL_WILDCARD('${PYTHONARCHDIR}', 'samba/**/*.py', flat=False) |