s3: Add s3 net python bindings
authorDavid Mulder <dmulder@suse.com>
Wed, 21 Oct 2020 15:40:32 +0000 (09:40 -0600)
committerDavid Mulder <dmulder@samba.org>
Wed, 21 Apr 2021 20:51:31 +0000 (20:51 +0000)
This adds python bindings for the s3 net ads
join and leave commands.

Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
source3/param/pyparam.h [new file with mode: 0644]
source3/param/pyparam_util.c [new file with mode: 0644]
source3/param/wscript_build
source3/utils/py_net.c [new file with mode: 0644]
source3/utils/py_net.h [new file with mode: 0644]
source3/utils/wscript_build

diff --git a/source3/param/pyparam.h b/source3/param/pyparam.h
new file mode 100644 (file)
index 0000000..8a40a9b
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+   Unix SMB/CIFS implementation.
+   Samba utility functions
+   Copyright (C) David Mulder <dmulder@samba.org> 2021
+
+   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/>.
+*/
+
+#ifndef _PYPARAM_H_
+#define _PYPARAM_H_
+
+#include "param/param.h"
+
+_PUBLIC_ struct loadparm_context *lpcfg_from_py_object(TALLOC_CTX *mem_ctx, PyObject *py_obj);
+
+#endif /* _PYPARAM_H_ */
diff --git a/source3/param/pyparam_util.c b/source3/param/pyparam_util.c
new file mode 100644 (file)
index 0000000..67ab494
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+   Unix SMB/CIFS implementation.
+   Samba utility functions
+   Copyright (C) David Mulder <dmulder@samba.org> 2021
+
+   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/>.
+*/
+
+#include <Python.h>
+#include "includes.h"
+#include "param/param.h"
+#include "param/pyparam.h"
+#include "param/loadparm.h"
+#include "param/s3_param.h"
+#include <pytalloc.h>
+
+#define PyErr_FromString(str) Py_BuildValue("(s)", discard_const_p(char, str))
+#define PyLoadparmContext_AsLoadparmContext(obj) pytalloc_get_type(obj, struct loadparm_context)
+
+_PUBLIC_ struct loadparm_context *lpcfg_from_py_object(TALLOC_CTX *mem_ctx, PyObject *py_obj)
+{
+       PyObject *param_mod;
+       PyTypeObject *lp_type;
+       bool is_lpobj;
+       const struct loadparm_s3_helpers *s3_context;
+       struct loadparm_context *s4_context;
+
+       if (py_obj == Py_None) {
+               s3_context = loadparm_s3_helpers();
+
+               s4_context = loadparm_init_s3(mem_ctx, s3_context);
+               if (s4_context == NULL) {
+                       PyErr_NoMemory();
+                       return NULL;
+               }
+
+               if (!lpcfg_load_default(s4_context)) {
+                       PyErr_FromString("Failed to load defaults\n");
+                       return NULL;
+               }
+
+               return s4_context;
+       }
+
+       param_mod = PyImport_ImportModule("samba.param");
+       if (param_mod == NULL) {
+               return NULL;
+       }
+
+       lp_type = (PyTypeObject *)PyObject_GetAttrString(param_mod, "LoadParm");
+       Py_DECREF(param_mod);
+       if (lp_type == NULL) {
+               PyErr_SetString(PyExc_RuntimeError, "Unable to import LoadParm");
+               return NULL;
+       }
+
+       is_lpobj = PyObject_TypeCheck(py_obj, lp_type);
+       Py_DECREF(lp_type);
+       if (is_lpobj) {
+               return talloc_reference(mem_ctx, PyLoadparmContext_AsLoadparmContext(py_obj));
+       }
+
+       PyErr_SetNone(PyExc_TypeError);
+       return NULL;
+}
index 8a374c466c04e17370444a521bbb944be7a8318a..22faf5cdcce5c57d1a75dda73a46aab6ac3e5233 100644 (file)
@@ -15,12 +15,22 @@ bld.SAMBA_GENERATOR('s3_param_proto_h',
                     rule='${PYTHON} ${SRC[0].abspath(env)} --file ${SRC[1].abspath(env)} --output ${TGT} --mode=S3PROTO')
 
 pytalloc_util = bld.pyembed_libname('pytalloc-util')
+pyparam_util = bld.pyembed_libname('pyparam3_util')
+libpython = bld.pyembed_libname('LIBPYTHON')
+
 bld.SAMBA3_PYTHON('pys3param',
                   source='pyparam.c',
-                  deps='smbconf',
+                  deps='smbconf %s' % pyparam_util,
                   public_deps=' '.join(['samba-hostconfig', pytalloc_util, 'talloc']),
                   realname='samba/samba3/param.so')
 
+bld.SAMBA3_SUBSYSTEM(pyparam_util,
+                source='pyparam_util.c',
+                deps='%s samba-hostconfig %s' % (libpython, pytalloc_util),
+                pyext=True,
+                enabled=bld.PYTHON_BUILD_IS_ENABLED()
+                )
+
 bld.SAMBA3_SUBSYSTEM('param_service',
                      source='service.c',
                      deps = 'USER_UTIL smbconf PRINTING')
diff --git a/source3/utils/py_net.c b/source3/utils/py_net.c
new file mode 100644 (file)
index 0000000..1d9b1d7
--- /dev/null
@@ -0,0 +1,349 @@
+/*
+   Unix SMB/CIFS implementation.
+   Samba python bindings to s3 libnet library
+
+   Copyright (C) David Mulder <dmulder@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/>.
+*/
+
+#include <Python.h>
+#include "includes.h"
+#include <pytalloc.h>
+#include "python/modules.h"
+#include "python/py3compat.h"
+#include "rpc_client/rpc_client.h"
+#include <sys/socket.h>
+#include "net.h"
+#include "auth/credentials/credentials.h"
+#include "auth/credentials/pycredentials.h"
+#include "lib/cmdline_contexts.h"
+#include "param/loadparm.h"
+#include "param/s3_param.h"
+#include "param/pyparam.h"
+#include "py_net.h"
+#include "librpc/gen_ndr/libnet_join.h"
+#include "libnet/libnet_join.h"
+#include "libcli/security/dom_sid.h"
+#include "dynconfig/dynconfig.h"
+
+static WERROR check_ads_config(struct loadparm_context *lp_ctx)
+{
+       if (lpcfg_server_role(lp_ctx) != ROLE_DOMAIN_MEMBER ) {
+               d_printf(_("Host is not configured as a member server.\n"));
+               return WERR_INVALID_DOMAIN_ROLE;
+       }
+
+       if (strlen(lpcfg_netbios_name(lp_ctx)) > 15) {
+               d_printf(_("Our netbios name can be at most 15 chars long, "
+                          "\"%s\" is %u chars long\n"), lpcfg_netbios_name(lp_ctx),
+                        (unsigned int)strlen(lpcfg_netbios_name(lp_ctx)));
+               return WERR_INVALID_COMPUTERNAME;
+       }
+
+       if ( lpcfg_security(lp_ctx) == SEC_ADS && !*lpcfg_realm(lp_ctx)) {
+               d_fprintf(stderr, _("realm must be set in in %s for ADS "
+                         "join to succeed.\n"), get_dyn_CONFIGFILE());
+               return WERR_INVALID_PARAMETER;
+       }
+
+       return WERR_OK;
+}
+
+static PyObject *py_net_join_member(py_net_Object *self, PyObject *args, PyObject *kwargs)
+{
+       struct libnet_JoinCtx *r = NULL;
+       WERROR werr;
+       PyObject *result;
+       TALLOC_CTX *mem_ctx;
+       bool modify_config = lp_config_backend_is_registry();
+       const char *kwnames[] = { "dnshostname", "createupn", "createcomputer",
+                                 "osName", "osVer", "osServicePack",
+                                 "machinepass", "debug", NULL };
+
+       mem_ctx = talloc_new(self->mem_ctx);
+       if (mem_ctx == NULL) {
+               PyErr_NoMemory();
+               return NULL;
+       }
+
+       werr = libnet_init_JoinCtx(mem_ctx, &r);
+       if (!W_ERROR_IS_OK(werr)) {
+               PyErr_NoMemory();
+               return NULL;
+       }
+
+       if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|sssssssp:Join",
+                                        discard_const_p(char *, kwnames),
+                                        &r->in.dnshostname,
+                                        &r->in.upn,
+                                        &r->in.account_ou,
+                                        &r->in.os_name,
+                                        &r->in.os_version,
+                                        &r->in.os_servicepack,
+                                        &r->in.machine_password,
+                                        &r->in.debug)) {
+               talloc_free(mem_ctx);
+               PyErr_FromString(_("Invalid arguments\n"));
+               return NULL;
+       }
+
+       if (!modify_config) {
+               werr = check_ads_config(self->lp_ctx);
+               if (!W_ERROR_IS_OK(werr)) {
+                       PyErr_SetWERROR_and_string(werr,
+                               _("Invalid configuration.  Exiting....\n"));
+                       talloc_free(mem_ctx);
+                       return NULL;
+               }
+       }
+
+       r->in.domain_name       = lpcfg_realm(self->lp_ctx);
+       r->in.domain_name_type  = JoinDomNameTypeDNS;
+       r->in.create_upn        = r->in.upn != NULL ? true : false;
+       r->in.dc_name           = self->server_address;
+       r->in.admin_account     = cli_credentials_get_username(self->creds);
+       r->in.admin_password    = cli_credentials_get_password(self->creds);
+       r->in.use_kerberos      = cli_credentials_get_kerberos_state(self->creds);
+       r->in.modify_config     = modify_config;
+       r->in.join_flags        = WKSSVC_JOIN_FLAGS_JOIN_TYPE |
+                                 WKSSVC_JOIN_FLAGS_ACCOUNT_CREATE |
+                                 WKSSVC_JOIN_FLAGS_DOMAIN_JOIN_IF_JOINED;
+       r->in.msg_ctx           = cmdline_messaging_context(get_dyn_CONFIGFILE());
+
+       werr = libnet_Join(mem_ctx, r);
+       if (W_ERROR_EQUAL(werr, WERR_NERR_DCNOTFOUND)) {
+               r->in.domain_name = lpcfg_workgroup(self->lp_ctx);
+               r->in.domain_name_type = JoinDomNameTypeNBT;
+               werr = libnet_Join(mem_ctx, r);
+       }
+       if (!W_ERROR_IS_OK(werr)) {
+               PyErr_SetWERROR_and_string(werr,
+                                          r->out.error_string
+                                          ? r->out.error_string
+                                          : get_friendly_werror_msg(werr));
+               talloc_free(mem_ctx);
+               return NULL;
+       }
+
+       /*
+        * Check the short name of the domain
+        */
+
+       if (!modify_config && !strequal(lpcfg_workgroup(self->lp_ctx), r->out.netbios_domain_name)) {
+               d_printf(_("The workgroup in %s does not match the short\n"
+                          "domain name obtained from the server.\n"
+                          "Using the name [%s] from the server.\n"
+                          "You should set \"workgroup = %s\" in %s.\n"),
+                        get_dyn_CONFIGFILE(), r->out.netbios_domain_name,
+                        r->out.netbios_domain_name, get_dyn_CONFIGFILE());
+       }
+
+       result = Py_BuildValue("ss", dom_sid_string(mem_ctx, r->out.domain_sid),
+                              r->out.dns_domain_name);
+
+       talloc_free(mem_ctx);
+
+       return result;
+}
+
+static const char py_net_join_member_doc[] = "join_member(dnshostname, createupn, createcomputer, osName, osVer, osServicePack, machinepass) -> (domain_sid, domain_name)\n\n" \
+"Join the domain with the specified name.";
+
+static PyObject *py_net_leave(py_net_Object *self, PyObject *args, PyObject *kwargs)
+{
+       struct libnet_UnjoinCtx *r = NULL;
+       WERROR werr;
+       TALLOC_CTX *mem_ctx;
+       bool keep_account = false;
+       const char *kwnames[] = { "keepAccount", "debug", NULL };
+
+       mem_ctx = talloc_new(self->mem_ctx);
+       if (mem_ctx == NULL) {
+               PyErr_NoMemory();
+               return NULL;
+       }
+
+       if (!*lpcfg_realm(self->lp_ctx)) {
+               PyErr_FromString(_("No realm set, are we joined ?\n"));
+               return NULL;
+       }
+
+       werr = libnet_init_UnjoinCtx(mem_ctx, &r);
+       if (!W_ERROR_IS_OK(werr)) {
+               PyErr_SetWERROR_and_string(werr,
+                       _("Could not initialise unjoin context.\n"));
+               return NULL;
+       }
+
+       if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|pp:Leave",
+                                        discard_const_p(char *, kwnames),
+                                        &keep_account, &r->in.debug)) {
+               talloc_free(mem_ctx);
+               PyErr_FromString(_("Invalid arguments\n"));
+               return NULL;
+       }
+
+       r->in.use_kerberos      = cli_credentials_get_kerberos_state(self->creds);
+       r->in.dc_name           = self->server_address;
+       r->in.domain_name       = lpcfg_realm(self->lp_ctx);
+       r->in.admin_account     = cli_credentials_get_username(self->creds);
+       r->in.admin_password    = cli_credentials_get_password(self->creds);
+       r->in.modify_config     = lp_config_backend_is_registry();
+
+       /*
+        * Try to delete it, but if that fails, disable it.  The
+        * WKSSVC_JOIN_FLAGS_ACCOUNT_DELETE really means "disable"
+        */
+       r->in.unjoin_flags      = WKSSVC_JOIN_FLAGS_JOIN_TYPE |
+                                 WKSSVC_JOIN_FLAGS_ACCOUNT_DELETE;
+       if (keep_account) {
+               r->in.delete_machine_account = false;
+       } else {
+               r->in.delete_machine_account = true;
+       }
+
+       r->in.msg_ctx           = cmdline_messaging_context(get_dyn_CONFIGFILE());
+
+       werr = libnet_Unjoin(mem_ctx, r);
+       if (!W_ERROR_IS_OK(werr)) {
+               PyErr_SetWERROR_and_string(werr,
+                                          r->out.error_string
+                                          ? r->out.error_string
+                                          : get_friendly_werror_msg(werr));
+               Py_RETURN_FALSE;
+       }
+
+       if (r->out.deleted_machine_account) {
+               d_printf(_("Deleted account for '%s' in realm '%s'\n"),
+                       r->in.machine_name, r->out.dns_domain_name);
+               Py_RETURN_TRUE;
+       }
+
+       if (r->out.disabled_machine_account) {
+               d_printf(_("Disabled account for '%s' in realm '%s'\n"),
+                       r->in.machine_name, r->out.dns_domain_name);
+               werr = WERR_OK;
+               Py_RETURN_TRUE;
+       }
+
+       /*
+        * Based on what we requested, we shouldn't get here, but if
+        * we did, it means the secrets were removed, and therefore
+        * we have left the domain.
+        */
+       d_fprintf(stderr, _("Machine '%s' Left domain '%s'\n"),
+                 r->in.machine_name, r->out.dns_domain_name);
+       Py_RETURN_TRUE;
+}
+
+static const char py_net_leave_doc[] = "leave(keepAccount) -> success\n\n" \
+"Leave the joined domain.";
+
+static PyMethodDef net_obj_methods[] = {
+       {
+               .ml_name  = "join_member",
+               .ml_meth  = PY_DISCARD_FUNC_SIG(PyCFunction,
+                               py_net_join_member),
+               .ml_flags = METH_VARARGS|METH_KEYWORDS,
+               .ml_doc   = py_net_join_member_doc
+       },
+       {
+               .ml_name  = "leave",
+               .ml_meth  = PY_DISCARD_FUNC_SIG(PyCFunction,
+                               py_net_leave),
+               .ml_flags = METH_VARARGS|METH_KEYWORDS,
+               .ml_doc   = py_net_leave_doc
+       },
+       { .ml_name = NULL }
+};
+
+static void py_net_dealloc(py_net_Object *self)
+{
+       talloc_free(self->mem_ctx);
+       PyObject_Del(self);
+}
+
+static PyObject *net_obj_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
+{
+       PyObject *py_creds, *py_lp = Py_None;
+       const char *kwnames[] = { "creds", "lp", "server", NULL };
+       py_net_Object *ret;
+       const char *server_address = NULL;
+
+       if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|Oz",
+                                        discard_const_p(char *, kwnames), &py_creds, &py_lp,
+                                        &server_address)) {
+               PyErr_FromString(_("Invalid arguments\n"));
+               return NULL;
+       }
+
+       ret = PyObject_New(py_net_Object, type);
+       if (ret == NULL) {
+               return NULL;
+       }
+
+       ret->ev = samba_tevent_context_init(NULL);
+       ret->mem_ctx = talloc_stackframe();
+
+       ret->lp_ctx = lpcfg_from_py_object(ret->mem_ctx, py_lp);
+       if (ret->lp_ctx == NULL) {
+               Py_DECREF(ret);
+               return NULL;
+       }
+
+       ret->server_address = server_address;
+
+       ret->creds = cli_credentials_from_py_object(py_creds);
+       if (ret->creds == NULL) {
+               PyErr_SetString(PyExc_TypeError, "Expected credentials object");
+               Py_DECREF(ret);
+               return NULL;
+       }
+
+       return (PyObject *)ret;
+}
+
+
+PyTypeObject py_net_Type = {
+       PyVarObject_HEAD_INIT(NULL, 0)
+       .tp_name = "net_s3.Net",
+       .tp_basicsize = sizeof(py_net_Object),
+       .tp_dealloc = (destructor)py_net_dealloc,
+       .tp_methods = net_obj_methods,
+       .tp_new = net_obj_new,
+};
+
+static struct PyModuleDef moduledef = {
+       PyModuleDef_HEAD_INIT,
+       .m_name = "net",
+       .m_size = -1,
+};
+
+MODULE_INIT_FUNC(net_s3)
+{
+       PyObject *m;
+
+       if (PyType_Ready(&py_net_Type) < 0)
+               return NULL;
+
+       m = PyModule_Create(&moduledef);
+       if (m == NULL)
+               return NULL;
+
+       Py_INCREF(&py_net_Type);
+       PyModule_AddObject(m, "Net", (PyObject *)&py_net_Type);
+
+       return m;
+}
diff --git a/source3/utils/py_net.h b/source3/utils/py_net.h
new file mode 100644 (file)
index 0000000..9ace71b
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+   Unix SMB/CIFS implementation.
+   Samba python bindings to s3 libnet library
+
+   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/>.
+*/
+
+typedef struct {
+       PyObject_HEAD
+       TALLOC_CTX *mem_ctx;
+       struct cli_credentials *creds;
+       struct loadparm_context *lp_ctx;
+       const char *server_address;
+       struct tevent_context *ev;
+} py_net_Object;
index 5ab82660c452a34893ff127d8ed138eba7fa5924..4148e01d6d2cc5132dac61fc1ca1f097baf21259 100644 (file)
@@ -326,3 +326,11 @@ bld.SAMBA3_BINARY('mdsearch',
                  RPCCLI_MDSSVC
                  mdssvc
                  ''')
+
+pytalloc_util = bld.pyembed_libname('pytalloc-util')
+pyrpc_util = bld.pyembed_libname('pyrpc_util')
+bld.SAMBA3_PYTHON('python_net_s3',
+        source='py_net.c',
+        deps='LIBNET popt_samba3_cmdline %s %s' % (pytalloc_util, pyrpc_util),
+        realname='samba/net_s3.so'
+        )