[PATCH] samba-tool user/group/computer edit fixes and contact management

Björn Baumbach bb at sernet.de
Fri Mar 22 17:19:06 UTC 2019


Hi!

I did some work to improve the "edit" sub-command of the samba-tool and
need a review:

https://gitlab.com/samba-team/devel/samba/pipelines/53186176

There was an additional successful run a few hours ago, with nearly the
same code:
https://gitlab.com/samba-team/devel/samba/pipelines/53110583

The set of patches solve different things:

- Without the patches it is not possible to modify base64 encoded
  attribute values.
- Allow to edit attribute values as clear text, instead of
  base64, if possible.
- Add the group and computer edit commands, which were missing.
- Add new contact object management:

Usage: samba-tool contact <subcommand>

Contact management.

Available subcommands:
 create  - Create a new contact.
 delete  - Delete a contact.
 edit    - Modify a contact.
 list    - List all contacts.
 move    - Move a contact object to an organizational unit or container.
 show    - Display a contact.

Best regards,
Björn

-- 
SerNet GmbH, Bahnhofsallee 1b, 37081 Göttingen
phone: 0551-370000-0, mailto:kontakt at sernet.de
Gesch.F.: Dr. Johannes Loxen und Reinhild Jung
AG Göttingen: HR-B 2816 - http://www.sernet.de

Samba eXPerience 2019, Hotel Freizeit In
sponsored by Google, Microsoft & Red Hat
June, 4th - 6th 2019, http://sambaXP.org
-------------- next part --------------
From 86adc7c4b94f265dcca8e60f1577d0d571507d19 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B6rn=20Baumbach?= <bb at sernet.de>
Date: Thu, 21 Mar 2019 14:15:22 +0100
Subject: [PATCH 01/18] samba-tool: fix format of command description (help
 messages)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Need to quote the backslash '\'.

Signed-off-by: Björn Baumbach <bbaumbach at samba.org>
---
 python/samba/netcmd/computer.py |  6 +++---
 python/samba/netcmd/group.py    | 10 +++++-----
 python/samba/netcmd/ou.py       |  4 ++--
 python/samba/netcmd/schema.py   |  2 +-
 python/samba/netcmd/user.py     | 14 +++++++-------
 5 files changed, 18 insertions(+), 18 deletions(-)

diff --git a/python/samba/netcmd/computer.py b/python/samba/netcmd/computer.py
index ff4c3979e78..81b401db9b3 100644
--- a/python/samba/netcmd/computer.py
+++ b/python/samba/netcmd/computer.py
@@ -181,7 +181,7 @@ accounts are also referred to as security principals and are assigned a
 security identifier (SID).
 
 Example1:
-samba-tool computer create Computer1 -H ldap://samba.samdom.example.com \
+samba-tool computer create Computer1 -H ldap://samba.samdom.example.com \\
     -Uadministrator%passw1rd
 
 Example1 shows how to create a new computer in the domain against a remote LDAP
@@ -323,7 +323,7 @@ userid. The -H or --URL= option can be used to execute the command against
 a remote server.
 
 Example1:
-samba-tool computer delete Computer1 -H ldap://samba.samdom.example.com \
+samba-tool computer delete Computer1 -H ldap://samba.samdom.example.com \\
     -Uadministrator%passw1rd
 
 Example1 shows how to delete a computer in the domain against a remote LDAP
@@ -450,7 +450,7 @@ The -H or --URL= option can be used to execute the command against a remote
 server.
 
 Example1:
-samba-tool computer show Computer1 -H ldap://samba.samdom.example.com \
+samba-tool computer show Computer1 -H ldap://samba.samdom.example.com \\
     -U administrator
 
 Example1 shows how display a computers attributes in the domain against a
diff --git a/python/samba/netcmd/group.py b/python/samba/netcmd/group.py
index 3d55222e8d0..6976f82d132 100644
--- a/python/samba/netcmd/group.py
+++ b/python/samba/netcmd/group.py
@@ -444,7 +444,7 @@ class cmd_group_move(Command):
     server.
 
     Example1:
-    samba-tool group move Group1 'OU=OrgUnit,DC=samdom.DC=example,DC=com' \
+    samba-tool group move Group1 'OU=OrgUnit,DC=samdom.DC=example,DC=com' \\
         -H ldap://samba.samdom.example.com -U administrator
 
     Example1 shows how to move a group Group1 into the 'OrgUnit' organizational
@@ -522,11 +522,11 @@ The -H or --URL= option can be used to execute the command against a remote
 server.
 
 Example1:
-samba-tool group show Group1 -H ldap://samba.samdom.example.com \
--U administrator --password=passw1rd
+samba-tool group show Group1 -H ldap://samba.samdom.example.com \\
+    -U administrator --password=passw1rd
 
-Example1 shows how to display a group's attributes in the domain against a remote
-LDAP server.
+Example1 shows how to display a group's attributes in the domain against a
+remote LDAP server.
 
 The -H parameter is used to specify the remote target server.
 
diff --git a/python/samba/netcmd/ou.py b/python/samba/netcmd/ou.py
index f4e01838e6e..ecc4582cbcf 100644
--- a/python/samba/netcmd/ou.py
+++ b/python/samba/netcmd/ou.py
@@ -38,7 +38,7 @@ class cmd_rename(Command):
     or without the domainDN component.
 
     Examples:
-    samba-tool ou rename 'OU=OrgUnit,DC=samdom,DC=example,DC=com' \
+    samba-tool ou rename 'OU=OrgUnit,DC=samdom,DC=example,DC=com' \\
         'OU=NewNameOfOrgUnit,DC=samdom,DC=example,DC=com'
     samba-tool ou rename 'OU=OrgUnit' 'OU=NewNameOfOrgUnit'
 
@@ -102,7 +102,7 @@ class cmd_move(Command):
     or without the domainDN component.
 
     Examples:
-    samba-tool ou move 'OU=OrgUnit,DC=samdom,DC=example,DC=com' \
+    samba-tool ou move 'OU=OrgUnit,DC=samdom,DC=example,DC=com' \\
         'OU=NewParentOfOrgUnit,DC=samdom,DC=example,DC=com'
     samba-tool ou rename 'OU=OrgUnit' 'OU=NewParentOfOrgUnit'
 
diff --git a/python/samba/netcmd/schema.py b/python/samba/netcmd/schema.py
index 889dd3fb539..d322da015ae 100644
--- a/python/samba/netcmd/schema.py
+++ b/python/samba/netcmd/schema.py
@@ -38,7 +38,7 @@ class cmd_schema_attribute_modify(Command):
     so be sure to view the current content before making changes.
 
     Example1:
-    samba-tool schema attribute modify uid \
+    samba-tool schema attribute modify uid \\
         --searchflags="fATTINDEX,fPRESERVEONDELETE"
 
     This alters the uid attribute to be indexed and to be preserved when
diff --git a/python/samba/netcmd/user.py b/python/samba/netcmd/user.py
index 8ead8e583f3..a64d2176dfa 100644
--- a/python/samba/netcmd/user.py
+++ b/python/samba/netcmd/user.py
@@ -291,8 +291,8 @@ samba-tool user create User4 passw4rd --rfc2307-from-nss --gecos 'some text'
 Example4 shows how to create a new user with Unix UID, GID and login-shell set from the local NSS and GECOS set to 'some text'.
 
 Example5:
-samba-tool user create User5 passw5rd --nis-domain=samdom --unix-home=/home/User5 \
-           --uid-number=10005 --login-shell=/bin/false --gid-number=10000
+samba-tool user create User5 passw5rd --nis-domain=samdom --unix-home=/home/User5 \\
+    --uid-number=10005 --login-shell=/bin/false --gid-number=10000
 
 Example5 shows how to create an RFC2307/NIS domain enabled user account. If
 --nis-domain is set, then the other four parameters are mandatory.
@@ -2389,8 +2389,8 @@ The -H or --URL= option can be used to execute the command against a remote
 server.
 
 Example1:
-samba-tool user edit User1 -H ldap://samba.samdom.example.com \
--U administrator --password=passw1rd
+samba-tool user edit User1 -H ldap://samba.samdom.example.com \\
+    -U administrator --password=passw1rd
 
 Example1 shows how to edit a users attributes in the domain against a remote
 LDAP server.
@@ -2522,8 +2522,8 @@ The -H or --URL= option can be used to execute the command against a remote
 server.
 
 Example1:
-samba-tool user show User1 -H ldap://samba.samdom.example.com \
--U administrator --password=passw1rd
+samba-tool user show User1 -H ldap://samba.samdom.example.com \\
+    -U administrator --password=passw1rd
 
 Example1 shows how to display a users attributes in the domain against a remote
 LDAP server.
@@ -2603,7 +2603,7 @@ class cmd_user_move(Command):
     server.
 
     Example1:
-    samba-tool user move User1 'OU=OrgUnit,DC=samdom.DC=example,DC=com' \
+    samba-tool user move User1 'OU=OrgUnit,DC=samdom.DC=example,DC=com' \\
         -H ldap://samba.samdom.example.com -U administrator
 
     Example1 shows how to move a user User1 into the 'OrgUnit' organizational
-- 
2.19.2


From 8048255b89e58c597dc9bc41b6874e57f9ff3f81 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B6rn=20Baumbach?= <bb at sernet.de>
Date: Thu, 14 Mar 2019 16:47:36 +0100
Subject: [PATCH 02/18] samba-tool tests: rename "user edit" test from edit.sh
 to user_edit.sh
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Björn Baumbach <bbaumbach at samba.org>
---
 python/samba/tests/samba_tool/{edit.sh => user_edit.sh} | 2 +-
 source4/selftest/tests.py                               | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)
 rename python/samba/tests/samba_tool/{edit.sh => user_edit.sh} (97%)

diff --git a/python/samba/tests/samba_tool/edit.sh b/python/samba/tests/samba_tool/user_edit.sh
similarity index 97%
rename from python/samba/tests/samba_tool/edit.sh
rename to python/samba/tests/samba_tool/user_edit.sh
index aca4cc247eb..3bbad411b82 100755
--- a/python/samba/tests/samba_tool/edit.sh
+++ b/python/samba/tests/samba_tool/user_edit.sh
@@ -4,7 +4,7 @@
 
 if [ $# -lt 3 ]; then
 cat <<EOF
-Usage: edit.sh SERVER USERNAME PASSWORD
+Usage: user_edit.sh SERVER USERNAME PASSWORD
 EOF
 exit 1;
 fi
diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py
index c6355f3a41f..b0c4ce581b3 100755
--- a/source4/selftest/tests.py
+++ b/source4/selftest/tests.py
@@ -657,7 +657,7 @@ for env in all_fl_envs:
 # test user.edit
 for env in all_fl_envs:
     env += ":local"
-    plantestsuite("samba.tests.samba_tool.edit", env, [os.path.join(srcdir(), "python/samba/tests/samba_tool/edit.sh"), '$SERVER', '$USERNAME', '$PASSWORD'])
+    plantestsuite("samba.tests.samba_tool.user_edit", env, [os.path.join(srcdir(), "python/samba/tests/samba_tool/user_edit.sh"), '$SERVER', '$USERNAME', '$PASSWORD'])
 
 # We run this test against both AD DC implementations because it is
 # the only test we have of GPO get/set behaviour, and this involves
-- 
2.19.2


From 97c9f43932ba4075910bbc077d4e370cc26106e1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B6rn=20Baumbach?= <bb at sernet.de>
Date: Thu, 14 Mar 2019 12:29:13 +0100
Subject: [PATCH 03/18] samba-tool tests: remove probably outdated comment
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Björn Baumbach <bbaumbach at samba.org>
---
 python/samba/tests/samba_tool/user_edit.sh | 1 -
 1 file changed, 1 deletion(-)

diff --git a/python/samba/tests/samba_tool/user_edit.sh b/python/samba/tests/samba_tool/user_edit.sh
index 3bbad411b82..66ae1d00721 100755
--- a/python/samba/tests/samba_tool/user_edit.sh
+++ b/python/samba/tests/samba_tool/user_edit.sh
@@ -17,7 +17,6 @@ STpath=$(pwd)
 . $STpath/testprogs/blackbox/subunit.sh
 
 # create editor.sh
-# this has to be hard linked to /tmp or 'samba-tool user edit' cannot find it
 tmpeditor=$(mktemp --suffix .sh -p $STpath/bin samba-tool-editor-XXXXXXXX)
 
 cat >$tmpeditor <<-'EOF'
-- 
2.19.2


From d2af90fdb2a8a15a051a91ad8492e0b07549552f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B6rn=20Baumbach?= <bb at sernet.de>
Date: Thu, 14 Mar 2019 16:55:42 +0100
Subject: [PATCH 04/18] samba-tool user edit test: use testit instead of
 subunit_start_test, pass/failed
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Björn Baumbach <bbaumbach at samba.org>
---
 python/samba/tests/samba_tool/user_edit.sh | 61 ++++++++--------------
 1 file changed, 23 insertions(+), 38 deletions(-)

diff --git a/python/samba/tests/samba_tool/user_edit.sh b/python/samba/tests/samba_tool/user_edit.sh
index 66ae1d00721..166d45ae4d6 100755
--- a/python/samba/tests/samba_tool/user_edit.sh
+++ b/python/samba/tests/samba_tool/user_edit.sh
@@ -16,55 +16,40 @@ PASSWORD="$3"
 STpath=$(pwd)
 . $STpath/testprogs/blackbox/subunit.sh
 
-# create editor.sh
 tmpeditor=$(mktemp --suffix .sh -p $STpath/bin samba-tool-editor-XXXXXXXX)
+chmod +x $tmpeditor
+
+create_test_user() {
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool \
+		user create sambatool1 --random-password \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
 
-cat >$tmpeditor <<-'EOF'
+edit_user() {
+	# create editor.sh
+	cat >$tmpeditor <<-'EOF'
 #!/usr/bin/env bash
 user_ldif="$1"
 SED=$(which sed)
 $SED -i -e 's/userAccountControl: 512/userAccountControl: 514/' $user_ldif
 EOF
 
-chmod +x $tmpeditor
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool \
+	user edit sambatool1 --editor=$tmpeditor \
+	-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
 
-failed=0
+delete_user() {
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool \
+		user delete sambatool1 \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
 
-# Create a test user
-subunit_start_test "Create_User"
-output=$($PYTHON ${STpath}/source4/scripting/bin/samba-tool user create sambatool1 --random-password \
--H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD")
-status=$?
-if [ "x$status" = "x0" ]; then
-    subunit_pass_test "Create_User"
-else
-    echo "$output" | subunit_fail_test "Create_User"
-    failed=$((failed + 1))
-fi
-
-# Edit test user
-subunit_start_test "Edit_User"
-output=$($PYTHON ${STpath}/source4/scripting/bin/samba-tool user edit sambatool1 --editor=$tmpeditor \
--H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD")
-status=$?
-if [ "x$status" = "x0" ]; then
-    subunit_pass_test "Edit_User"
-else
-    echo "$output" | subunit_fail_test "Edit_User"
-    failed=$((failed + 1))
-fi
+failed=0
 
-# Delete test user
-subunit_start_test "Delete_User"
-output=$($PYTHON ${STpath}/source4/scripting/bin/samba-tool user delete sambatool1 \
--H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD")
-status=$?
-if [ "x$status" = "x0" ]; then
-    subunit_pass_test "Delete_User"
-else
-    echo "$output" | subunit_fail_test "Delete_User"
-    failed=$((failed + 1))
-fi
+testit "create_test_user" create_test_user || failed=`expr $failed + 1`
+testit "edit_user" edit_user || failed=`expr $failed + 1`
+testit "delete_user" delete_user || failed=`expr $failed + 1`
 
 rm -f $tmpeditor
 
-- 
2.19.2


From 8d14531c3119c878bd5666db3afb46074fbf9134 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B6rn=20Baumbach?= <bb at sernet.de>
Date: Tue, 19 Feb 2019 12:14:37 +0100
Subject: [PATCH 05/18] ldb/ldb_ldif: add copy_raw_bytes helper variable to
 ldb_ldif_write_trace()
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Björn Baumbach <bbaumbach at samba.org>
---
 lib/ldb/common/ldb_ldif.c | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/lib/ldb/common/ldb_ldif.c b/lib/ldb/common/ldb_ldif.c
index e69467891c9..90bddd2f43d 100644
--- a/lib/ldb/common/ldb_ldif.c
+++ b/lib/ldb/common/ldb_ldif.c
@@ -352,6 +352,7 @@ static int ldb_ldif_write_trace(struct ldb_context *ldb,
 		for (j=0;j<msg->elements[i].num_values;j++) {
 			struct ldb_val v;
 			bool use_b64_encode = false;
+			bool copy_raw_bytes = false;
 
 			ret = a->syntax->ldif_write_fn(ldb, mem_ctx, &msg->elements[i].values[j], &v);
 			if (ret != LDB_SUCCESS) {
@@ -360,6 +361,7 @@ static int ldb_ldif_write_trace(struct ldb_context *ldb,
 
 			if (ldb->flags & LDB_FLG_SHOW_BINARY) {
 				use_b64_encode = false;
+				copy_raw_bytes = true;
 			} else if (a->flags & LDB_ATTR_FLAG_FORCE_BASE64_LDIF) {
 				use_b64_encode = true;
 			} else {
@@ -379,7 +381,7 @@ static int ldb_ldif_write_trace(struct ldb_context *ldb,
 			} else {
 				ret = fprintf_fn(private_data, "%s: ", msg->elements[i].name);
 				CHECK_RET;
-				if (ldb->flags & LDB_FLG_SHOW_BINARY) {
+				if (copy_raw_bytes) {
 					ret = fprintf_fn(private_data, "%*.*s",
 							 v.length, v.length, (char *)v.data);
 				} else {
-- 
2.19.2


From db342cca31cb0728f0da390a98aec52e961afa61 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B6rn=20Baumbach?= <bb at sernet.de>
Date: Tue, 19 Feb 2019 12:29:58 +0100
Subject: [PATCH 06/18] ldb/ldb_ldif: add LDB_FLAG_FORCE_NO_BASE64_LDIF flag
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Flag is used to enforce binary encoded attribute values per attribute.

Signed-off-by: Björn Baumbach <bbaumbach at samba.org>
---
 lib/ldb/common/ldb_ldif.c | 4 ++++
 lib/ldb/include/ldb.h     | 6 ++++++
 lib/ldb/pyldb.c           | 1 +
 3 files changed, 11 insertions(+)

diff --git a/lib/ldb/common/ldb_ldif.c b/lib/ldb/common/ldb_ldif.c
index 90bddd2f43d..56be9e6574e 100644
--- a/lib/ldb/common/ldb_ldif.c
+++ b/lib/ldb/common/ldb_ldif.c
@@ -364,6 +364,10 @@ static int ldb_ldif_write_trace(struct ldb_context *ldb,
 				copy_raw_bytes = true;
 			} else if (a->flags & LDB_ATTR_FLAG_FORCE_BASE64_LDIF) {
 				use_b64_encode = true;
+			} else if (msg->elements[i].flags &
+			           LDB_FLAG_FORCE_NO_BASE64_LDIF) {
+				use_b64_encode = false;
+				copy_raw_bytes = true;
 			} else {
 				use_b64_encode = ldb_should_b64_encode(ldb, &v);
 			}
diff --git a/lib/ldb/include/ldb.h b/lib/ldb/include/ldb.h
index 81bee934da5..9349dbaa500 100644
--- a/lib/ldb/include/ldb.h
+++ b/lib/ldb/include/ldb.h
@@ -139,6 +139,12 @@ struct ldb_dn;
 */
 #define LDB_FLAG_MOD_DELETE  3
 
+/**
+   Flag value used in ldb_ldif_write_trace() to enforce binary encoded
+   attribute values per attribute.
+*/
+#define LDB_FLAG_FORCE_NO_BASE64_LDIF 4
+
 /**
     flag bits on an element usable only by the internal implementation
 */
diff --git a/lib/ldb/pyldb.c b/lib/ldb/pyldb.c
index 10a4b6cb55d..e41c3b7fe2a 100644
--- a/lib/ldb/pyldb.c
+++ b/lib/ldb/pyldb.c
@@ -4323,6 +4323,7 @@ static PyObject* module_init(void)
 	ADD_LDB_INT(FLAG_MOD_ADD);
 	ADD_LDB_INT(FLAG_MOD_REPLACE);
 	ADD_LDB_INT(FLAG_MOD_DELETE);
+	ADD_LDB_INT(FLAG_FORCE_NO_BASE64_LDIF);
 
 	ADD_LDB_INT(ATTR_FLAG_HIDDEN);
 	ADD_LDB_INT(ATTR_FLAG_UNIQUE_INDEX);
-- 
2.19.2


From c1b3bcb6cba50073eb846922f7b3f35ebe245927 Mon Sep 17 00:00:00 2001
From: Stefan Metzmacher <metze at samba.org>
Date: Fri, 15 Mar 2019 15:27:36 +0100
Subject: [PATCH 07/18] ldb: version 1.6.3

* Enable make test even without lmdb
* Add LDB_FLAG_FORCE_NO_BASE64_LDIF

Signed-off-by: Stefan Metzmacher <metze at samba.org>
---
 lib/ldb/ABI/ldb-1.6.3.sigs            | 280 ++++++++++++++++++++++++++
 lib/ldb/ABI/pyldb-util-1.6.3.sigs     |   2 +
 lib/ldb/ABI/pyldb-util.py3-1.6.3.sigs |   2 +
 lib/ldb/wscript                       |   2 +-
 4 files changed, 285 insertions(+), 1 deletion(-)
 create mode 100644 lib/ldb/ABI/ldb-1.6.3.sigs
 create mode 100644 lib/ldb/ABI/pyldb-util-1.6.3.sigs
 create mode 100644 lib/ldb/ABI/pyldb-util.py3-1.6.3.sigs

diff --git a/lib/ldb/ABI/ldb-1.6.3.sigs b/lib/ldb/ABI/ldb-1.6.3.sigs
new file mode 100644
index 00000000000..0c1234f1c97
--- /dev/null
+++ b/lib/ldb/ABI/ldb-1.6.3.sigs
@@ -0,0 +1,280 @@
+ldb_add: int (struct ldb_context *, const struct ldb_message *)
+ldb_any_comparison: int (struct ldb_context *, void *, ldb_attr_handler_t, const struct ldb_val *, const struct ldb_val *)
+ldb_asprintf_errstring: void (struct ldb_context *, const char *, ...)
+ldb_attr_casefold: char *(TALLOC_CTX *, const char *)
+ldb_attr_dn: int (const char *)
+ldb_attr_in_list: int (const char * const *, const char *)
+ldb_attr_list_copy: const char **(TALLOC_CTX *, const char * const *)
+ldb_attr_list_copy_add: const char **(TALLOC_CTX *, const char * const *, const char *)
+ldb_base64_decode: int (char *)
+ldb_base64_encode: char *(TALLOC_CTX *, const char *, int)
+ldb_binary_decode: struct ldb_val (TALLOC_CTX *, const char *)
+ldb_binary_encode: char *(TALLOC_CTX *, struct ldb_val)
+ldb_binary_encode_string: char *(TALLOC_CTX *, const char *)
+ldb_build_add_req: int (struct ldb_request **, struct ldb_context *, TALLOC_CTX *, const struct ldb_message *, struct ldb_control **, void *, ldb_request_callback_t, struct ldb_request *)
+ldb_build_del_req: int (struct ldb_request **, struct ldb_context *, TALLOC_CTX *, struct ldb_dn *, struct ldb_control **, void *, ldb_request_callback_t, struct ldb_request *)
+ldb_build_extended_req: int (struct ldb_request **, struct ldb_context *, TALLOC_CTX *, const char *, void *, struct ldb_control **, void *, ldb_request_callback_t, struct ldb_request *)
+ldb_build_mod_req: int (struct ldb_request **, struct ldb_context *, TALLOC_CTX *, const struct ldb_message *, struct ldb_control **, void *, ldb_request_callback_t, struct ldb_request *)
+ldb_build_rename_req: int (struct ldb_request **, struct ldb_context *, TALLOC_CTX *, struct ldb_dn *, struct ldb_dn *, struct ldb_control **, void *, ldb_request_callback_t, struct ldb_request *)
+ldb_build_search_req: int (struct ldb_request **, struct ldb_context *, TALLOC_CTX *, struct ldb_dn *, enum ldb_scope, const char *, const char * const *, struct ldb_control **, void *, ldb_request_callback_t, struct ldb_request *)
+ldb_build_search_req_ex: int (struct ldb_request **, struct ldb_context *, TALLOC_CTX *, struct ldb_dn *, enum ldb_scope, struct ldb_parse_tree *, const char * const *, struct ldb_control **, void *, ldb_request_callback_t, struct ldb_request *)
+ldb_casefold: char *(struct ldb_context *, TALLOC_CTX *, const char *, size_t)
+ldb_casefold_default: char *(void *, TALLOC_CTX *, const char *, size_t)
+ldb_check_critical_controls: int (struct ldb_control **)
+ldb_comparison_binary: int (struct ldb_context *, void *, const struct ldb_val *, const struct ldb_val *)
+ldb_comparison_fold: int (struct ldb_context *, void *, const struct ldb_val *, const struct ldb_val *)
+ldb_connect: int (struct ldb_context *, const char *, unsigned int, const char **)
+ldb_control_to_string: char *(TALLOC_CTX *, const struct ldb_control *)
+ldb_controls_except_specified: struct ldb_control **(struct ldb_control **, TALLOC_CTX *, struct ldb_control *)
+ldb_debug: void (struct ldb_context *, enum ldb_debug_level, const char *, ...)
+ldb_debug_add: void (struct ldb_context *, const char *, ...)
+ldb_debug_end: void (struct ldb_context *, enum ldb_debug_level)
+ldb_debug_set: void (struct ldb_context *, enum ldb_debug_level, const char *, ...)
+ldb_delete: int (struct ldb_context *, struct ldb_dn *)
+ldb_dn_add_base: bool (struct ldb_dn *, struct ldb_dn *)
+ldb_dn_add_base_fmt: bool (struct ldb_dn *, const char *, ...)
+ldb_dn_add_child: bool (struct ldb_dn *, struct ldb_dn *)
+ldb_dn_add_child_fmt: bool (struct ldb_dn *, const char *, ...)
+ldb_dn_add_child_val: bool (struct ldb_dn *, const char *, struct ldb_val)
+ldb_dn_alloc_casefold: char *(TALLOC_CTX *, struct ldb_dn *)
+ldb_dn_alloc_linearized: char *(TALLOC_CTX *, struct ldb_dn *)
+ldb_dn_canonical_ex_string: char *(TALLOC_CTX *, struct ldb_dn *)
+ldb_dn_canonical_string: char *(TALLOC_CTX *, struct ldb_dn *)
+ldb_dn_check_local: bool (struct ldb_module *, struct ldb_dn *)
+ldb_dn_check_special: bool (struct ldb_dn *, const char *)
+ldb_dn_compare: int (struct ldb_dn *, struct ldb_dn *)
+ldb_dn_compare_base: int (struct ldb_dn *, struct ldb_dn *)
+ldb_dn_copy: struct ldb_dn *(TALLOC_CTX *, struct ldb_dn *)
+ldb_dn_escape_value: char *(TALLOC_CTX *, struct ldb_val)
+ldb_dn_extended_add_syntax: int (struct ldb_context *, unsigned int, const struct ldb_dn_extended_syntax *)
+ldb_dn_extended_filter: void (struct ldb_dn *, const char * const *)
+ldb_dn_extended_syntax_by_name: const struct ldb_dn_extended_syntax *(struct ldb_context *, const char *)
+ldb_dn_from_ldb_val: struct ldb_dn *(TALLOC_CTX *, struct ldb_context *, const struct ldb_val *)
+ldb_dn_get_casefold: const char *(struct ldb_dn *)
+ldb_dn_get_comp_num: int (struct ldb_dn *)
+ldb_dn_get_component_name: const char *(struct ldb_dn *, unsigned int)
+ldb_dn_get_component_val: const struct ldb_val *(struct ldb_dn *, unsigned int)
+ldb_dn_get_extended_comp_num: int (struct ldb_dn *)
+ldb_dn_get_extended_component: const struct ldb_val *(struct ldb_dn *, const char *)
+ldb_dn_get_extended_linearized: char *(TALLOC_CTX *, struct ldb_dn *, int)
+ldb_dn_get_ldb_context: struct ldb_context *(struct ldb_dn *)
+ldb_dn_get_linearized: const char *(struct ldb_dn *)
+ldb_dn_get_parent: struct ldb_dn *(TALLOC_CTX *, struct ldb_dn *)
+ldb_dn_get_rdn_name: const char *(struct ldb_dn *)
+ldb_dn_get_rdn_val: const struct ldb_val *(struct ldb_dn *)
+ldb_dn_has_extended: bool (struct ldb_dn *)
+ldb_dn_is_null: bool (struct ldb_dn *)
+ldb_dn_is_special: bool (struct ldb_dn *)
+ldb_dn_is_valid: bool (struct ldb_dn *)
+ldb_dn_map_local: struct ldb_dn *(struct ldb_module *, void *, struct ldb_dn *)
+ldb_dn_map_rebase_remote: struct ldb_dn *(struct ldb_module *, void *, struct ldb_dn *)
+ldb_dn_map_remote: struct ldb_dn *(struct ldb_module *, void *, struct ldb_dn *)
+ldb_dn_minimise: bool (struct ldb_dn *)
+ldb_dn_new: struct ldb_dn *(TALLOC_CTX *, struct ldb_context *, const char *)
+ldb_dn_new_fmt: struct ldb_dn *(TALLOC_CTX *, struct ldb_context *, const char *, ...)
+ldb_dn_remove_base_components: bool (struct ldb_dn *, unsigned int)
+ldb_dn_remove_child_components: bool (struct ldb_dn *, unsigned int)
+ldb_dn_remove_extended_components: void (struct ldb_dn *)
+ldb_dn_replace_components: bool (struct ldb_dn *, struct ldb_dn *)
+ldb_dn_set_component: int (struct ldb_dn *, int, const char *, const struct ldb_val)
+ldb_dn_set_extended_component: int (struct ldb_dn *, const char *, const struct ldb_val *)
+ldb_dn_update_components: int (struct ldb_dn *, const struct ldb_dn *)
+ldb_dn_validate: bool (struct ldb_dn *)
+ldb_dump_results: void (struct ldb_context *, struct ldb_result *, FILE *)
+ldb_error_at: int (struct ldb_context *, int, const char *, const char *, int)
+ldb_errstring: const char *(struct ldb_context *)
+ldb_extended: int (struct ldb_context *, const char *, void *, struct ldb_result **)
+ldb_extended_default_callback: int (struct ldb_request *, struct ldb_reply *)
+ldb_filter_from_tree: char *(TALLOC_CTX *, const struct ldb_parse_tree *)
+ldb_get_config_basedn: struct ldb_dn *(struct ldb_context *)
+ldb_get_create_perms: unsigned int (struct ldb_context *)
+ldb_get_default_basedn: struct ldb_dn *(struct ldb_context *)
+ldb_get_event_context: struct tevent_context *(struct ldb_context *)
+ldb_get_flags: unsigned int (struct ldb_context *)
+ldb_get_opaque: void *(struct ldb_context *, const char *)
+ldb_get_root_basedn: struct ldb_dn *(struct ldb_context *)
+ldb_get_schema_basedn: struct ldb_dn *(struct ldb_context *)
+ldb_global_init: int (void)
+ldb_handle_get_event_context: struct tevent_context *(struct ldb_handle *)
+ldb_handle_new: struct ldb_handle *(TALLOC_CTX *, struct ldb_context *)
+ldb_handle_use_global_event_context: void (struct ldb_handle *)
+ldb_handler_copy: int (struct ldb_context *, void *, const struct ldb_val *, struct ldb_val *)
+ldb_handler_fold: int (struct ldb_context *, void *, const struct ldb_val *, struct ldb_val *)
+ldb_init: struct ldb_context *(TALLOC_CTX *, struct tevent_context *)
+ldb_ldif_message_redacted_string: char *(struct ldb_context *, TALLOC_CTX *, enum ldb_changetype, const struct ldb_message *)
+ldb_ldif_message_string: char *(struct ldb_context *, TALLOC_CTX *, enum ldb_changetype, const struct ldb_message *)
+ldb_ldif_parse_modrdn: int (struct ldb_context *, const struct ldb_ldif *, TALLOC_CTX *, struct ldb_dn **, struct ldb_dn **, bool *, struct ldb_dn **, struct ldb_dn **)
+ldb_ldif_read: struct ldb_ldif *(struct ldb_context *, int (*)(void *), void *)
+ldb_ldif_read_file: struct ldb_ldif *(struct ldb_context *, FILE *)
+ldb_ldif_read_file_state: struct ldb_ldif *(struct ldb_context *, struct ldif_read_file_state *)
+ldb_ldif_read_free: void (struct ldb_context *, struct ldb_ldif *)
+ldb_ldif_read_string: struct ldb_ldif *(struct ldb_context *, const char **)
+ldb_ldif_write: int (struct ldb_context *, int (*)(void *, const char *, ...), void *, const struct ldb_ldif *)
+ldb_ldif_write_file: int (struct ldb_context *, FILE *, const struct ldb_ldif *)
+ldb_ldif_write_redacted_trace_string: char *(struct ldb_context *, TALLOC_CTX *, const struct ldb_ldif *)
+ldb_ldif_write_string: char *(struct ldb_context *, TALLOC_CTX *, const struct ldb_ldif *)
+ldb_load_modules: int (struct ldb_context *, const char **)
+ldb_map_add: int (struct ldb_module *, struct ldb_request *)
+ldb_map_delete: int (struct ldb_module *, struct ldb_request *)
+ldb_map_init: int (struct ldb_module *, const struct ldb_map_attribute *, const struct ldb_map_objectclass *, const char * const *, const char *, const char *)
+ldb_map_modify: int (struct ldb_module *, struct ldb_request *)
+ldb_map_rename: int (struct ldb_module *, struct ldb_request *)
+ldb_map_search: int (struct ldb_module *, struct ldb_request *)
+ldb_match_message: int (struct ldb_context *, const struct ldb_message *, const struct ldb_parse_tree *, enum ldb_scope, bool *)
+ldb_match_msg: int (struct ldb_context *, const struct ldb_message *, const struct ldb_parse_tree *, struct ldb_dn *, enum ldb_scope)
+ldb_match_msg_error: int (struct ldb_context *, const struct ldb_message *, const struct ldb_parse_tree *, struct ldb_dn *, enum ldb_scope, bool *)
+ldb_match_msg_objectclass: int (const struct ldb_message *, const char *)
+ldb_mod_register_control: int (struct ldb_module *, const char *)
+ldb_modify: int (struct ldb_context *, const struct ldb_message *)
+ldb_modify_default_callback: int (struct ldb_request *, struct ldb_reply *)
+ldb_module_call_chain: char *(struct ldb_request *, TALLOC_CTX *)
+ldb_module_connect_backend: int (struct ldb_context *, const char *, const char **, struct ldb_module **)
+ldb_module_done: int (struct ldb_request *, struct ldb_control **, struct ldb_extended *, int)
+ldb_module_flags: uint32_t (struct ldb_context *)
+ldb_module_get_ctx: struct ldb_context *(struct ldb_module *)
+ldb_module_get_name: const char *(struct ldb_module *)
+ldb_module_get_ops: const struct ldb_module_ops *(struct ldb_module *)
+ldb_module_get_private: void *(struct ldb_module *)
+ldb_module_init_chain: int (struct ldb_context *, struct ldb_module *)
+ldb_module_load_list: int (struct ldb_context *, const char **, struct ldb_module *, struct ldb_module **)
+ldb_module_new: struct ldb_module *(TALLOC_CTX *, struct ldb_context *, const char *, const struct ldb_module_ops *)
+ldb_module_next: struct ldb_module *(struct ldb_module *)
+ldb_module_popt_options: struct poptOption **(struct ldb_context *)
+ldb_module_send_entry: int (struct ldb_request *, struct ldb_message *, struct ldb_control **)
+ldb_module_send_referral: int (struct ldb_request *, char *)
+ldb_module_set_next: void (struct ldb_module *, struct ldb_module *)
+ldb_module_set_private: void (struct ldb_module *, void *)
+ldb_modules_hook: int (struct ldb_context *, enum ldb_module_hook_type)
+ldb_modules_list_from_string: const char **(struct ldb_context *, TALLOC_CTX *, const char *)
+ldb_modules_load: int (const char *, const char *)
+ldb_msg_add: int (struct ldb_message *, const struct ldb_message_element *, int)
+ldb_msg_add_empty: int (struct ldb_message *, const char *, int, struct ldb_message_element **)
+ldb_msg_add_fmt: int (struct ldb_message *, const char *, const char *, ...)
+ldb_msg_add_linearized_dn: int (struct ldb_message *, const char *, struct ldb_dn *)
+ldb_msg_add_steal_string: int (struct ldb_message *, const char *, char *)
+ldb_msg_add_steal_value: int (struct ldb_message *, const char *, struct ldb_val *)
+ldb_msg_add_string: int (struct ldb_message *, const char *, const char *)
+ldb_msg_add_value: int (struct ldb_message *, const char *, const struct ldb_val *, struct ldb_message_element **)
+ldb_msg_canonicalize: struct ldb_message *(struct ldb_context *, const struct ldb_message *)
+ldb_msg_check_string_attribute: int (const struct ldb_message *, const char *, const char *)
+ldb_msg_copy: struct ldb_message *(TALLOC_CTX *, const struct ldb_message *)
+ldb_msg_copy_attr: int (struct ldb_message *, const char *, const char *)
+ldb_msg_copy_shallow: struct ldb_message *(TALLOC_CTX *, const struct ldb_message *)
+ldb_msg_diff: struct ldb_message *(struct ldb_context *, struct ldb_message *, struct ldb_message *)
+ldb_msg_difference: int (struct ldb_context *, TALLOC_CTX *, struct ldb_message *, struct ldb_message *, struct ldb_message **)
+ldb_msg_element_compare: int (struct ldb_message_element *, struct ldb_message_element *)
+ldb_msg_element_compare_name: int (struct ldb_message_element *, struct ldb_message_element *)
+ldb_msg_element_equal_ordered: bool (const struct ldb_message_element *, const struct ldb_message_element *)
+ldb_msg_find_attr_as_bool: int (const struct ldb_message *, const char *, int)
+ldb_msg_find_attr_as_dn: struct ldb_dn *(struct ldb_context *, TALLOC_CTX *, const struct ldb_message *, const char *)
+ldb_msg_find_attr_as_double: double (const struct ldb_message *, const char *, double)
+ldb_msg_find_attr_as_int: int (const struct ldb_message *, const char *, int)
+ldb_msg_find_attr_as_int64: int64_t (const struct ldb_message *, const char *, int64_t)
+ldb_msg_find_attr_as_string: const char *(const struct ldb_message *, const char *, const char *)
+ldb_msg_find_attr_as_uint: unsigned int (const struct ldb_message *, const char *, unsigned int)
+ldb_msg_find_attr_as_uint64: uint64_t (const struct ldb_message *, const char *, uint64_t)
+ldb_msg_find_common_values: int (struct ldb_context *, TALLOC_CTX *, struct ldb_message_element *, struct ldb_message_element *, uint32_t)
+ldb_msg_find_duplicate_val: int (struct ldb_context *, TALLOC_CTX *, const struct ldb_message_element *, struct ldb_val **, uint32_t)
+ldb_msg_find_element: struct ldb_message_element *(const struct ldb_message *, const char *)
+ldb_msg_find_ldb_val: const struct ldb_val *(const struct ldb_message *, const char *)
+ldb_msg_find_val: struct ldb_val *(const struct ldb_message_element *, struct ldb_val *)
+ldb_msg_new: struct ldb_message *(TALLOC_CTX *)
+ldb_msg_normalize: int (struct ldb_context *, TALLOC_CTX *, const struct ldb_message *, struct ldb_message **)
+ldb_msg_remove_attr: void (struct ldb_message *, const char *)
+ldb_msg_remove_element: void (struct ldb_message *, struct ldb_message_element *)
+ldb_msg_rename_attr: int (struct ldb_message *, const char *, const char *)
+ldb_msg_sanity_check: int (struct ldb_context *, const struct ldb_message *)
+ldb_msg_sort_elements: void (struct ldb_message *)
+ldb_next_del_trans: int (struct ldb_module *)
+ldb_next_end_trans: int (struct ldb_module *)
+ldb_next_init: int (struct ldb_module *)
+ldb_next_prepare_commit: int (struct ldb_module *)
+ldb_next_read_lock: int (struct ldb_module *)
+ldb_next_read_unlock: int (struct ldb_module *)
+ldb_next_remote_request: int (struct ldb_module *, struct ldb_request *)
+ldb_next_request: int (struct ldb_module *, struct ldb_request *)
+ldb_next_start_trans: int (struct ldb_module *)
+ldb_op_default_callback: int (struct ldb_request *, struct ldb_reply *)
+ldb_options_find: const char *(struct ldb_context *, const char **, const char *)
+ldb_pack_data: int (struct ldb_context *, const struct ldb_message *, struct ldb_val *)
+ldb_parse_control_from_string: struct ldb_control *(struct ldb_context *, TALLOC_CTX *, const char *)
+ldb_parse_control_strings: struct ldb_control **(struct ldb_context *, TALLOC_CTX *, const char **)
+ldb_parse_tree: struct ldb_parse_tree *(TALLOC_CTX *, const char *)
+ldb_parse_tree_attr_replace: void (struct ldb_parse_tree *, const char *, const char *)
+ldb_parse_tree_copy_shallow: struct ldb_parse_tree *(TALLOC_CTX *, const struct ldb_parse_tree *)
+ldb_parse_tree_walk: int (struct ldb_parse_tree *, int (*)(struct ldb_parse_tree *, void *), void *)
+ldb_qsort: void (void * const, size_t, size_t, void *, ldb_qsort_cmp_fn_t)
+ldb_register_backend: int (const char *, ldb_connect_fn, bool)
+ldb_register_extended_match_rule: int (struct ldb_context *, const struct ldb_extended_match_rule *)
+ldb_register_hook: int (ldb_hook_fn)
+ldb_register_module: int (const struct ldb_module_ops *)
+ldb_rename: int (struct ldb_context *, struct ldb_dn *, struct ldb_dn *)
+ldb_reply_add_control: int (struct ldb_reply *, const char *, bool, void *)
+ldb_reply_get_control: struct ldb_control *(struct ldb_reply *, const char *)
+ldb_req_get_custom_flags: uint32_t (struct ldb_request *)
+ldb_req_is_untrusted: bool (struct ldb_request *)
+ldb_req_location: const char *(struct ldb_request *)
+ldb_req_mark_trusted: void (struct ldb_request *)
+ldb_req_mark_untrusted: void (struct ldb_request *)
+ldb_req_set_custom_flags: void (struct ldb_request *, uint32_t)
+ldb_req_set_location: void (struct ldb_request *, const char *)
+ldb_request: int (struct ldb_context *, struct ldb_request *)
+ldb_request_add_control: int (struct ldb_request *, const char *, bool, void *)
+ldb_request_done: int (struct ldb_request *, int)
+ldb_request_get_control: struct ldb_control *(struct ldb_request *, const char *)
+ldb_request_get_status: int (struct ldb_request *)
+ldb_request_replace_control: int (struct ldb_request *, const char *, bool, void *)
+ldb_request_set_state: void (struct ldb_request *, int)
+ldb_reset_err_string: void (struct ldb_context *)
+ldb_save_controls: int (struct ldb_control *, struct ldb_request *, struct ldb_control ***)
+ldb_schema_attribute_add: int (struct ldb_context *, const char *, unsigned int, const char *)
+ldb_schema_attribute_add_with_syntax: int (struct ldb_context *, const char *, unsigned int, const struct ldb_schema_syntax *)
+ldb_schema_attribute_by_name: const struct ldb_schema_attribute *(struct ldb_context *, const char *)
+ldb_schema_attribute_fill_with_syntax: int (struct ldb_context *, TALLOC_CTX *, const char *, unsigned int, const struct ldb_schema_syntax *, struct ldb_schema_attribute *)
+ldb_schema_attribute_remove: void (struct ldb_context *, const char *)
+ldb_schema_attribute_remove_flagged: void (struct ldb_context *, unsigned int)
+ldb_schema_attribute_set_override_handler: void (struct ldb_context *, ldb_attribute_handler_override_fn_t, void *)
+ldb_schema_set_override_GUID_index: void (struct ldb_context *, const char *, const char *)
+ldb_schema_set_override_indexlist: void (struct ldb_context *, bool)
+ldb_search: int (struct ldb_context *, TALLOC_CTX *, struct ldb_result **, struct ldb_dn *, enum ldb_scope, const char * const *, const char *, ...)
+ldb_search_default_callback: int (struct ldb_request *, struct ldb_reply *)
+ldb_sequence_number: int (struct ldb_context *, enum ldb_sequence_type, uint64_t *)
+ldb_set_create_perms: void (struct ldb_context *, unsigned int)
+ldb_set_debug: int (struct ldb_context *, void (*)(void *, enum ldb_debug_level, const char *, va_list), void *)
+ldb_set_debug_stderr: int (struct ldb_context *)
+ldb_set_default_dns: void (struct ldb_context *)
+ldb_set_errstring: void (struct ldb_context *, const char *)
+ldb_set_event_context: void (struct ldb_context *, struct tevent_context *)
+ldb_set_flags: void (struct ldb_context *, unsigned int)
+ldb_set_modules_dir: void (struct ldb_context *, const char *)
+ldb_set_opaque: int (struct ldb_context *, const char *, void *)
+ldb_set_require_private_event_context: void (struct ldb_context *)
+ldb_set_timeout: int (struct ldb_context *, struct ldb_request *, int)
+ldb_set_timeout_from_prev_req: int (struct ldb_context *, struct ldb_request *, struct ldb_request *)
+ldb_set_utf8_default: void (struct ldb_context *)
+ldb_set_utf8_fns: void (struct ldb_context *, void *, char *(*)(void *, void *, const char *, size_t))
+ldb_setup_wellknown_attributes: int (struct ldb_context *)
+ldb_should_b64_encode: int (struct ldb_context *, const struct ldb_val *)
+ldb_standard_syntax_by_name: const struct ldb_schema_syntax *(struct ldb_context *, const char *)
+ldb_strerror: const char *(int)
+ldb_string_to_time: time_t (const char *)
+ldb_string_utc_to_time: time_t (const char *)
+ldb_timestring: char *(TALLOC_CTX *, time_t)
+ldb_timestring_utc: char *(TALLOC_CTX *, time_t)
+ldb_transaction_cancel: int (struct ldb_context *)
+ldb_transaction_cancel_noerr: int (struct ldb_context *)
+ldb_transaction_commit: int (struct ldb_context *)
+ldb_transaction_prepare_commit: int (struct ldb_context *)
+ldb_transaction_start: int (struct ldb_context *)
+ldb_unpack_data: int (struct ldb_context *, const struct ldb_val *, struct ldb_message *)
+ldb_unpack_data_only_attr_list: int (struct ldb_context *, const struct ldb_val *, struct ldb_message *, const char * const *, unsigned int, unsigned int *)
+ldb_unpack_data_only_attr_list_flags: int (struct ldb_context *, const struct ldb_val *, struct ldb_message *, const char * const *, unsigned int, unsigned int, unsigned int *)
+ldb_val_dup: struct ldb_val (TALLOC_CTX *, const struct ldb_val *)
+ldb_val_equal_exact: int (const struct ldb_val *, const struct ldb_val *)
+ldb_val_map_local: struct ldb_val (struct ldb_module *, void *, const struct ldb_map_attribute *, const struct ldb_val *)
+ldb_val_map_remote: struct ldb_val (struct ldb_module *, void *, const struct ldb_map_attribute *, const struct ldb_val *)
+ldb_val_string_cmp: int (const struct ldb_val *, const char *)
+ldb_val_to_time: int (const struct ldb_val *, time_t *)
+ldb_valid_attr_name: int (const char *)
+ldb_vdebug: void (struct ldb_context *, enum ldb_debug_level, const char *, va_list)
+ldb_wait: int (struct ldb_handle *, enum ldb_wait_type)
diff --git a/lib/ldb/ABI/pyldb-util-1.6.3.sigs b/lib/ldb/ABI/pyldb-util-1.6.3.sigs
new file mode 100644
index 00000000000..74d6719d2bc
--- /dev/null
+++ b/lib/ldb/ABI/pyldb-util-1.6.3.sigs
@@ -0,0 +1,2 @@
+pyldb_Dn_FromDn: PyObject *(struct ldb_dn *)
+pyldb_Object_AsDn: bool (TALLOC_CTX *, PyObject *, struct ldb_context *, struct ldb_dn **)
diff --git a/lib/ldb/ABI/pyldb-util.py3-1.6.3.sigs b/lib/ldb/ABI/pyldb-util.py3-1.6.3.sigs
new file mode 100644
index 00000000000..74d6719d2bc
--- /dev/null
+++ b/lib/ldb/ABI/pyldb-util.py3-1.6.3.sigs
@@ -0,0 +1,2 @@
+pyldb_Dn_FromDn: PyObject *(struct ldb_dn *)
+pyldb_Object_AsDn: bool (TALLOC_CTX *, PyObject *, struct ldb_context *, struct ldb_dn **)
diff --git a/lib/ldb/wscript b/lib/ldb/wscript
index 8f14b09b583..f48724e4d6c 100644
--- a/lib/ldb/wscript
+++ b/lib/ldb/wscript
@@ -1,7 +1,7 @@
 #!/usr/bin/env python
 
 APPNAME = 'ldb'
-VERSION = '1.6.2'
+VERSION = '1.6.3'
 
 import sys, os
 
-- 
2.19.2


From 166d3c34d492bef977f80013e336f4c5e52eaa83 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B6rn=20Baumbach?= <bb at sernet.de>
Date: Thu, 14 Mar 2019 14:04:28 +0100
Subject: [PATCH 08/18] samba-tool tests: add additional tests for "samba-tool
 user edit" command
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Especially test handling of base64 encoded attribute values here.

Add selftest/knownfail.d/samba_tool.user_edit.
Tests fail, because:
 - can not work with ldif without a trailing new line
 - can not handle base64 strings

Signed-off-by: Björn Baumbach <bbaumbach at samba.org>
---
 python/samba/tests/samba_tool/user_edit.sh | 97 ++++++++++++++++++++++
 selftest/knownfail.d/samba_tool.user_edit  |  3 +
 2 files changed, 100 insertions(+)
 create mode 100644 selftest/knownfail.d/samba_tool.user_edit

diff --git a/python/samba/tests/samba_tool/user_edit.sh b/python/samba/tests/samba_tool/user_edit.sh
index 166d45ae4d6..0535efedbdd 100755
--- a/python/samba/tests/samba_tool/user_edit.sh
+++ b/python/samba/tests/samba_tool/user_edit.sh
@@ -16,6 +16,13 @@ PASSWORD="$3"
 STpath=$(pwd)
 . $STpath/testprogs/blackbox/subunit.sh
 
+display_name="Björn"
+display_name_b64="QmrDtnJu"
+display_name_new="Renamed Bjoern"
+# attribute value including control character
+# echo -e "test \a string" | base64
+display_name_con_b64="dGVzdCAHIHN0cmluZwo="
+
 tmpeditor=$(mktemp --suffix .sh -p $STpath/bin samba-tool-editor-XXXXXXXX)
 chmod +x $tmpeditor
 
@@ -39,6 +46,89 @@ EOF
 	-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
 }
 
+# Test edit user - add base64 attributes
+add_attribute_base64() {
+	# create editor.sh
+	cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+user_ldif="\$1"
+
+grep -v '^$' \$user_ldif > \${user_ldif}.tmp
+echo "displayName:: $display_name_b64" >> \${user_ldif}.tmp
+
+mv \${user_ldif}.tmp \$user_ldif
+EOF
+
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool user edit \
+		sambatool1 --editor=$tmpeditor \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_attribute_base64() {
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool user show \
+		sambatool1 --attributes=displayName \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+delete_attribute() {
+	# create editor.sh
+	cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+user_ldif="\$1"
+
+grep -v '^displayName' \$user_ldif >> \${user_ldif}.tmp
+mv \${user_ldif}.tmp \$user_ldif
+EOF
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool user edit \
+		sambatool1 --editor=$tmpeditor \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+# Test edit user - add base64 attribute value including control character
+add_attribute_base64_control() {
+	# create editor.sh
+	cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+user_ldif="\$1"
+
+grep -v '^$' \$user_ldif > \${user_ldif}.tmp
+echo "displayName:: $display_name_con_b64" >> \${user_ldif}.tmp
+
+mv \${user_ldif}.tmp \$user_ldif
+EOF
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool user edit \
+		sambatool1 --editor=$tmpeditor \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_attribute_base64_control() {
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool user show \
+		sambatool1 --attributes=displayName \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+
+# Test edit user - change base64 attribute value including control character
+change_attribute_base64_control() {
+	# create editor.sh
+	cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+user_ldif="\$1"
+
+sed -i -e 's/displayName:: $display_name_con_b64/displayName: $display_name/' \
+	\$user_ldif
+EOF
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool user edit \
+		sambatool1 --editor=$tmpeditor \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_attribute_base64_control() {
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool user show \
+		sambatool1 --attributes=displayName \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
 delete_user() {
 	$PYTHON ${STpath}/source4/scripting/bin/samba-tool \
 		user delete sambatool1 \
@@ -49,6 +139,13 @@ failed=0
 
 testit "create_test_user" create_test_user || failed=`expr $failed + 1`
 testit "edit_user" edit_user || failed=`expr $failed + 1`
+testit "add_attribute_base64" add_attribute_base64 || failed=`expr $failed + 1`
+testit_grep "get_attribute_base64" "^displayName:: $display_name_b64" get_attribute_base64 || failed=`expr $failed + 1`
+testit "delete_attribute" delete_attribute || failed=`expr $failed + 1`
+testit "add_attribute_base64_control" add_attribute_base64_control || failed=`expr $failed + 1`
+testit_grep "get_attribute_base64_control" "^displayName:: $display_name_con_b64" get_attribute_base64_control || failed=`expr $failed + 1`
+testit "change_attribute_base64_control" change_attribute_base64_control || failed=`expr $failed + 1`
+testit_grep "get_attribute_base64_control" "^displayName:: $display_name_b64" get_attribute_base64_control || failed=`expr $failed + 1`
 testit "delete_user" delete_user || failed=`expr $failed + 1`
 
 rm -f $tmpeditor
diff --git a/selftest/knownfail.d/samba_tool.user_edit b/selftest/knownfail.d/samba_tool.user_edit
new file mode 100644
index 00000000000..46a1f2b5853
--- /dev/null
+++ b/selftest/knownfail.d/samba_tool.user_edit
@@ -0,0 +1,3 @@
+samba.tests.samba_tool.user_edit.add_attribute_base64
+samba.tests.samba_tool.user_edit.add_attribute_base64_control
+samba.tests.samba_tool.user_edit.change_attribute_base64_control
-- 
2.19.2


From 0559f11c71474a0fbe3c057cd63d7a5427a0fb46 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B6rn=20Baumbach?= <bb at sernet.de>
Date: Fri, 15 Mar 2019 14:19:35 +0100
Subject: [PATCH 09/18] samba-tool user edit: use ldb methods to create ldif to
 modify user
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Remove tests from knownfail:
  samba.tests.samba_tool.user_edit.add_attribute_base64
  samba.tests.samba_tool.user_edit.add_attribute_base64_control
  samba.tests.samba_tool.user_edit.change_attribute_base64_control

Signed-off-by: Björn Baumbach <bbaumbach at samba.org>
---
 python/samba/netcmd/user.py               | 51 ++++++-----------------
 selftest/knownfail.d/samba_tool.user_edit |  3 --
 2 files changed, 13 insertions(+), 41 deletions(-)
 delete mode 100644 selftest/knownfail.d/samba_tool.user_edit

diff --git a/python/samba/netcmd/user.py b/python/samba/netcmd/user.py
index a64d2176dfa..28ff617f12b 100644
--- a/python/samba/netcmd/user.py
+++ b/python/samba/netcmd/user.py
@@ -2467,46 +2467,21 @@ LDAP server using the 'nano' editor.
                 with open(t_file.name) as edited_file:
                     edited_message = edited_file.read()
 
-        if result_ldif != edited_message:
-            diff = difflib.ndiff(result_ldif.splitlines(),
-                                 edited_message.splitlines())
-            minus_lines = []
-            plus_lines = []
-            for line in diff:
-                if line.startswith('-'):
-                    line = line[2:]
-                    minus_lines.append(line)
-                elif line.startswith('+'):
-                    line = line[2:]
-                    plus_lines.append(line)
-
-            user_ldif = "dn: %s\n" % user_dn
-            user_ldif += "changetype: modify\n"
-
-            for line in minus_lines:
-                attr, val = line.split(':', 1)
-                search_attr = "%s:" % attr
-                if not re.search(r'^' + search_attr, str(plus_lines)):
-                    user_ldif += "delete: %s\n" % attr
-                    user_ldif += "%s: %s\n" % (attr, val)
-
-            for line in plus_lines:
-                attr, val = line.split(':', 1)
-                search_attr = "%s:" % attr
-                if re.search(r'^' + search_attr, str(minus_lines)):
-                    user_ldif += "replace: %s\n" % attr
-                    user_ldif += "%s: %s\n" % (attr, val)
-                if not re.search(r'^' + search_attr, str(minus_lines)):
-                    user_ldif += "add: %s\n" % attr
-                    user_ldif += "%s: %s\n" % (attr, val)
 
-            try:
-                samdb.modify_ldif(user_ldif)
-            except Exception as e:
-                raise CommandError("Failed to modify user '%s': " %
-                                   username, e)
+        msgs_edited = samdb.parse_ldif(edited_message)
+        msg_edited = next(msgs_edited)[1]
+
+        res_msg_diff = samdb.msg_diff(msg, msg_edited)
+        if len(res_msg_diff) == 0:
+            self.outf.write("Nothing to do\n")
+            return
+
+        try:
+            samdb.modify(res_msg_diff)
+        except Exception as e:
+            raise CommandError("Failed to modify user '%s': " % username, e)
 
-            self.outf.write("Modified User '%s' successfully\n" % username)
+        self.outf.write("Modified User '%s' successfully\n" % username)
 
 
 class cmd_user_show(Command):
diff --git a/selftest/knownfail.d/samba_tool.user_edit b/selftest/knownfail.d/samba_tool.user_edit
deleted file mode 100644
index 46a1f2b5853..00000000000
--- a/selftest/knownfail.d/samba_tool.user_edit
+++ /dev/null
@@ -1,3 +0,0 @@
-samba.tests.samba_tool.user_edit.add_attribute_base64
-samba.tests.samba_tool.user_edit.add_attribute_base64_control
-samba.tests.samba_tool.user_edit.change_attribute_base64_control
-- 
2.19.2


From bd2113e2017245bd4f06113dc9a28ef49dbff8be Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B6rn=20Baumbach?= <bb at sernet.de>
Date: Wed, 13 Mar 2019 17:40:37 +0100
Subject: [PATCH 10/18] samba-tool user edit: simplify code
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Use "None"-changetype here, instead of "Add". This avoids the need to
remove the changetype line afterwards.

Signed-off-by: Björn Baumbach <bbaumbach at samba.org>
---
 python/samba/netcmd/user.py | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/python/samba/netcmd/user.py b/python/samba/netcmd/user.py
index 28ff617f12b..112756ea4f5 100644
--- a/python/samba/netcmd/user.py
+++ b/python/samba/netcmd/user.py
@@ -2448,9 +2448,7 @@ LDAP server using the 'nano' editor.
             raise CommandError('Unable to find user "%s"' % (username))
 
         for msg in res:
-            r_ldif = samdb.write_ldif(msg, 1)
-            # remove 'changetype' line
-            result_ldif = re.sub('changetype: add\n', '', r_ldif)
+            result_ldif = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
 
             if editor is None:
                 editor = os.environ.get('EDITOR')
-- 
2.19.2


From e451c363f49841a0a0f8fa57d4a0cbdb9bdd364c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B6rn=20Baumbach?= <bb at sernet.de>
Date: Fri, 15 Mar 2019 12:59:09 +0100
Subject: [PATCH 11/18] samba-tool tests: add test for 'samba-tool user edit',
 using LDB_FLAG_FORCE_NO_BASE64_LDIF
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Test to edit a user: Change attributes with LDB_FLAG_FORCE_NO_BASE64_LDIF

Signed-off-by: Björn Baumbach <bbaumbach at samba.org>
---
 python/samba/tests/samba_tool/user_edit.sh | 26 ++++++++++++++++++++++
 selftest/knownfail.d/samba_tool.user_edit  |  1 +
 2 files changed, 27 insertions(+)
 create mode 100644 selftest/knownfail.d/samba_tool.user_edit

diff --git a/python/samba/tests/samba_tool/user_edit.sh b/python/samba/tests/samba_tool/user_edit.sh
index 0535efedbdd..03fbd61ff5d 100755
--- a/python/samba/tests/samba_tool/user_edit.sh
+++ b/python/samba/tests/samba_tool/user_edit.sh
@@ -129,6 +129,30 @@ get_attribute_base64_control() {
 		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
 }
 
+# Test edit user - change attributes with LDB_FLAG_FORCE_NO_BASE64_LDIF
+change_attribute_force_no_base64() {
+	# create editor.sh
+	# Expects that the original attribute is available as clear text,
+	# because the LDB_FLAG_FORCE_NO_BASE64_LDIF should be used here.
+	cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+user_ldif="\$1"
+
+sed -i -e 's/displayName: $display_name/displayName: $display_name_new/' \
+	\$user_ldif
+EOF
+
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool user edit \
+		sambatool1 --editor=$tmpeditor \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_changed_attribute_force_no_base64() {
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool user show \
+		 sambatool1 --attributes=displayName \
+		 -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
 delete_user() {
 	$PYTHON ${STpath}/source4/scripting/bin/samba-tool \
 		user delete sambatool1 \
@@ -146,6 +170,8 @@ testit "add_attribute_base64_control" add_attribute_base64_control || failed=`ex
 testit_grep "get_attribute_base64_control" "^displayName:: $display_name_con_b64" get_attribute_base64_control || failed=`expr $failed + 1`
 testit "change_attribute_base64_control" change_attribute_base64_control || failed=`expr $failed + 1`
 testit_grep "get_attribute_base64_control" "^displayName:: $display_name_b64" get_attribute_base64_control || failed=`expr $failed + 1`
+testit "change_attribute_force_no_base64" change_attribute_force_no_base64 || failed=`expr $failed + 1`
+testit_grep "get_changed_attribute_force_no_base64" "^displayName: $display_name_new" get_changed_attribute_force_no_base64 || failed=`expr $failed + 1`
 testit "delete_user" delete_user || failed=`expr $failed + 1`
 
 rm -f $tmpeditor
diff --git a/selftest/knownfail.d/samba_tool.user_edit b/selftest/knownfail.d/samba_tool.user_edit
new file mode 100644
index 00000000000..5c5c9a6d781
--- /dev/null
+++ b/selftest/knownfail.d/samba_tool.user_edit
@@ -0,0 +1 @@
+samba.tests.samba_tool.user_edit.change_attribute_force_no_base64
-- 
2.19.2


From 9c66b0c2408c4d7df1a02fbeb045f0ab72ed9729 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B6rn=20Baumbach?= <bb at sernet.de>
Date: Fri, 15 Mar 2019 14:20:05 +0100
Subject: [PATCH 12/18] samba-tool user edit: avoid base64 encoded strings in
 editable ldif if possible
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Use clear text arguments strings if possible. Makes it more comfortable
for users to edit the user objects attributes.

Remove test from knownfail:
  samba.tests.samba_tool.user_edit.change_attribute_force_no_base64

Signed-off-by: Björn Baumbach <bbaumbach at samba.org>
---
 python/samba/netcmd/common.py             | 45 ++++++++++++++++++++++-
 python/samba/netcmd/user.py               |  3 +-
 selftest/knownfail.d/samba_tool.user_edit |  1 -
 3 files changed, 46 insertions(+), 3 deletions(-)
 delete mode 100644 selftest/knownfail.d/samba_tool.user_edit

diff --git a/python/samba/netcmd/common.py b/python/samba/netcmd/common.py
index c68cbabf42e..664d3a83ac5 100644
--- a/python/samba/netcmd/common.py
+++ b/python/samba/netcmd/common.py
@@ -20,7 +20,7 @@
 import re
 from samba.dcerpc import nbt
 from samba.net import Net
-
+import ldb
 
 def _get_user_realm_domain(user):
     r""" get the realm or the domain and the base user
@@ -69,3 +69,46 @@ def netcmd_get_domain_infos_via_cldap(lp, creds, address=None):
     cldap_ret = net.finddc(address=address,
                            flags=nbt.NBT_SERVER_LDAP | nbt.NBT_SERVER_DS)
     return cldap_ret
+
+def is_printable_attr_val(val):
+    import unicodedata
+
+    # The value must be convertable to a string value.
+    try:
+        str_val = str(val)
+    except:
+        return False
+
+    # Characters of the Unicode Character Category "C" ("Other") are
+    # supposed to be not printable. The category "C" includes control
+    # characters, format specifier and others.
+    for c in str_val:
+        if unicodedata.category(c)[0] == 'C':
+            return False
+
+    return True
+
+def get_ldif_for_editor(samdb, msg):
+
+    # Copy the given message, because we do not
+    # want to modify the original message.
+    m = ldb.Message()
+    m.dn = msg.dn
+
+    for k in msg.keys():
+        if k == "dn":
+            continue
+        vals = msg[k]
+        m[k] = vals
+        need_base64 = False
+        for v in vals:
+            if is_printable_attr_val(v):
+                continue
+            need_base64 = True
+            break
+        if not need_base64:
+            m[k].set_flags(ldb.FLAG_FORCE_NO_BASE64_LDIF)
+
+    result_ldif = samdb.write_ldif(m, ldb.CHANGETYPE_NONE)
+
+    return result_ldif
diff --git a/python/samba/netcmd/user.py b/python/samba/netcmd/user.py
index 112756ea4f5..121050a26e6 100644
--- a/python/samba/netcmd/user.py
+++ b/python/samba/netcmd/user.py
@@ -2428,6 +2428,7 @@ LDAP server using the 'nano' editor.
 
     def run(self, username, credopts=None, sambaopts=None, versionopts=None,
             H=None, editor=None):
+        from . import common
 
         lp = sambaopts.get_loadparm()
         creds = credopts.get_credentials(lp, fallback_machine=True)
@@ -2448,7 +2449,7 @@ LDAP server using the 'nano' editor.
             raise CommandError('Unable to find user "%s"' % (username))
 
         for msg in res:
-            result_ldif = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
+            result_ldif = common.get_ldif_for_editor(samdb, msg)
 
             if editor is None:
                 editor = os.environ.get('EDITOR')
diff --git a/selftest/knownfail.d/samba_tool.user_edit b/selftest/knownfail.d/samba_tool.user_edit
deleted file mode 100644
index 5c5c9a6d781..00000000000
--- a/selftest/knownfail.d/samba_tool.user_edit
+++ /dev/null
@@ -1 +0,0 @@
-samba.tests.samba_tool.user_edit.change_attribute_force_no_base64
-- 
2.19.2


From 8ef8da57cfdc766a0f6fcadec224f855f14f6023 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B6rn=20Baumbach?= <bb at sernet.de>
Date: Wed, 13 Mar 2019 21:40:25 +0100
Subject: [PATCH 13/18] samba-tool computer: add 'edit' command to edit an AD
 computer object
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Similar to the samba-tool user edit command.

Signed-off-by: Björn Baumbach <bbaumbach at samba.org>
---
 python/samba/netcmd/computer.py | 123 +++++++++++++++++++++++++++++++-
 1 file changed, 122 insertions(+), 1 deletion(-)

diff --git a/python/samba/netcmd/computer.py b/python/samba/netcmd/computer.py
index 81b401db9b3..b66dcc4a7a5 100644
--- a/python/samba/netcmd/computer.py
+++ b/python/samba/netcmd/computer.py
@@ -25,6 +25,8 @@ import ldb
 import socket
 import samba
 import re
+import os
+import tempfile
 from samba import sd_utils
 from samba.dcerpc import dnsserver, dnsp, security
 from samba.dnsserver import ARecord, AAAARecord
@@ -32,6 +34,8 @@ from samba.ndr import ndr_unpack, ndr_pack, ndr_print
 from samba.remove_dc import remove_dns_references
 from samba.auth import system_session
 from samba.samdb import SamDB
+from samba.compat import get_bytes
+from subprocess import check_call, CalledProcessError
 
 from samba import (
     credentials,
@@ -48,7 +52,6 @@ from samba.netcmd import (
     Option,
 )
 
-
 def _is_valid_ip(ip_string, address_families=None):
     """Check ip string is valid address"""
     # by default, check both ipv4 and ipv6
@@ -400,6 +403,123 @@ sudo is used so a computer may run the command as root.
         self.outf.write("Deleted computer %s\n" % computername)
 
 
+class cmd_computer_edit(Command):
+    """Modify Computer AD object.
+
+    This command will allow editing of a computer account in the Active
+    Directory domain. You will then be able to add or change attributes and
+    their values.
+
+    The computername specified on the command is the sAMaccountName with or
+    without the trailing $ (dollar sign).
+
+    The command may be run from the root userid or another authorized userid.
+
+    The -H or --URL= option can be used to execute the command against a remote
+    server.
+
+    Example1:
+    samba-tool computer edit Computer1 -H ldap://samba.samdom.example.com \\
+        -U administrator --password=passw1rd
+
+    Example1 shows how to edit a computers attributes in the domain against a
+    remote LDAP server.
+
+    The -H parameter is used to specify the remote target server.
+
+    Example2:
+    samba-tool computer edit Computer2
+
+    Example2 shows how to edit a computers attributes in the domain against a
+    local LDAP server.
+
+    Example3:
+    samba-tool computer edit Computer3 --editor=nano
+
+    Example3 shows how to edit a computers attributes in the domain against a
+    local LDAP server using the 'nano' editor.
+    """
+    synopsis = "%prog <computername> [options]"
+
+    takes_options = [
+        Option("-H", "--URL", help="LDB URL for database or target server",
+               type=str, metavar="URL", dest="H"),
+        Option("--editor", help="Editor to use instead of the system default,"
+               " or 'vi' if no system default is set.", type=str),
+    ]
+
+    takes_args = ["computername"]
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "credopts": options.CredentialsOptions,
+        "versionopts": options.VersionOptions,
+    }
+
+    def run(self, computername, credopts=None, sambaopts=None, versionopts=None,
+            H=None, editor=None):
+        from . import common
+
+        lp = sambaopts.get_loadparm()
+        creds = credopts.get_credentials(lp, fallback_machine=True)
+        samdb = SamDB(url=H, session_info=system_session(),
+                      credentials=creds, lp=lp)
+
+        samaccountname = computername
+        if not computername.endswith('$'):
+            samaccountname = "%s$" % computername
+
+        filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
+                  (dsdb.ATYPE_WORKSTATION_TRUST,
+                   ldb.binary_encode(samaccountname)))
+
+        domaindn = samdb.domain_dn()
+
+        try:
+            res = samdb.search(base=domaindn,
+                               expression=filter,
+                               scope=ldb.SCOPE_SUBTREE)
+            computer_dn = res[0].dn
+        except IndexError:
+            raise CommandError('Unable to find computer "%s"' % (computername))
+
+        if len(res) != 1:
+            raise CommandError('Invalid number of results: for "%s": %d' %
+                               ((computername), len(res)))
+
+        msg = res[0]
+        result_ldif = common.get_ldif_for_editor(samdb, msg)
+
+        if editor is None:
+            editor = os.environ.get('EDITOR')
+            if editor is None:
+                editor = 'vi'
+
+        with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
+            t_file.write(get_bytes(result_ldif))
+            t_file.flush()
+            try:
+                check_call([editor, t_file.name])
+            except CalledProcessError as e:
+                raise CalledProcessError("ERROR: ", e)
+            with open(t_file.name) as edited_file:
+                edited_message = edited_file.read()
+
+        msgs_edited = samdb.parse_ldif(edited_message)
+        msg_edited = next(msgs_edited)[1]
+
+        res_msg_diff = samdb.msg_diff(msg, msg_edited)
+        if len(res_msg_diff) == 0:
+            self.outf.write("Nothing to do\n")
+            return
+
+        try:
+            samdb.modify(res_msg_diff)
+        except Exception as e:
+            raise CommandError("Failed to modify computer '%s': " %
+                               (computername, e))
+
+        self.outf.write("Modified computer '%s' successfully\n" % computername)
+
 class cmd_computer_list(Command):
     """List all computers."""
 
@@ -583,6 +703,7 @@ class cmd_computer(SuperCommand):
     subcommands = {}
     subcommands["create"] = cmd_computer_create()
     subcommands["delete"] = cmd_computer_delete()
+    subcommands["edit"] = cmd_computer_edit()
     subcommands["list"] = cmd_computer_list()
     subcommands["show"] = cmd_computer_show()
     subcommands["move"] = cmd_computer_move()
-- 
2.19.2


From 299f81143c376c39ab2fdf64b45c9b8800da8140 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B6rn=20Baumbach?= <bb at sernet.de>
Date: Mon, 18 Mar 2019 12:00:24 +0100
Subject: [PATCH 14/18] samba-tool tests: add test for 'samba-tool computer
 edit' command
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Björn Baumbach <bbaumbach at samba.org>
---
 .../samba/tests/samba_tool/computer_edit.sh   | 180 ++++++++++++++++++
 source4/selftest/tests.py                     |   3 +-
 2 files changed, 182 insertions(+), 1 deletion(-)
 create mode 100755 python/samba/tests/samba_tool/computer_edit.sh

diff --git a/python/samba/tests/samba_tool/computer_edit.sh b/python/samba/tests/samba_tool/computer_edit.sh
new file mode 100755
index 00000000000..fb6c668f2a3
--- /dev/null
+++ b/python/samba/tests/samba_tool/computer_edit.sh
@@ -0,0 +1,180 @@
+#!/bin/sh
+#
+# Test for 'samba-tool computer edit'
+
+if [ $# -lt 3 ]; then
+cat <<EOF
+Usage: computer_edit.sh SERVER USERNAME PASSWORD
+EOF
+exit 1;
+fi
+
+SERVER="$1"
+USERNAME="$2"
+PASSWORD="$3"
+
+STpath=$(pwd)
+. $STpath/testprogs/blackbox/subunit.sh
+
+display_name="Björns laptop"
+display_name_b64="QmrDtnJucyBsYXB0b3A="
+display_name_new="Bjoerns new laptop"
+# attribute value including control character
+# echo -e "test \a string" | base64
+display_name_con_b64="dGVzdCAHIHN0cmluZwo="
+
+tmpeditor=$(mktemp --suffix .sh -p $STpath/bin samba-tool-editor-XXXXXXXX)
+chmod +x $tmpeditor
+
+create_test_computer() {
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool \
+		computer create testmachine1 \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+edit_computer() {
+	# create editor.sh
+	# enable computer account
+	cat >$tmpeditor <<-'EOF'
+#!/usr/bin/env bash
+computer_ldif="$1"
+SED=$(which sed)
+$SED -i -e 's/userAccountControl: 4098/userAccountControl: 4096/' $computer_ldif
+EOF
+
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool \
+		computer edit testmachine1 --editor=$tmpeditor \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+# Test edit computer - add base64 attributes
+add_attribute_base64() {
+	# create editor.sh
+	cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+computer_ldif="\$1"
+
+grep -v '^$' \$computer_ldif > \${computer_ldif}.tmp
+echo "displayName:: $display_name_b64" >> \${computer_ldif}.tmp
+
+mv \${computer_ldif}.tmp \$computer_ldif
+EOF
+
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool computer edit \
+		testmachine1 --editor=$tmpeditor \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_attribute_base64() {
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool computer show \
+		testmachine1 --attributes=displayName \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+delete_attribute() {
+	# create editor.sh
+	cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+computer_ldif="\$1"
+
+grep -v '^displayName' \$computer_ldif >> \${computer_ldif}.tmp
+mv \${computer_ldif}.tmp \$computer_ldif
+EOF
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool computer edit \
+		testmachine1 --editor=$tmpeditor \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+# Test edit computer - add base64 attribute value including control character
+add_attribute_base64_control() {
+	# create editor.sh
+	cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+computer_ldif="\$1"
+
+grep -v '^$' \$computer_ldif > \${computer_ldif}.tmp
+echo "displayName:: $display_name_con_b64" >> \${computer_ldif}.tmp
+
+mv \${computer_ldif}.tmp \$computer_ldif
+EOF
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool computer edit \
+		testmachine1 --editor=$tmpeditor \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_attribute_base64_control() {
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool computer show \
+		testmachine1 --attributes=displayName \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+
+# Test edit computer - change base64 attribute value including control character
+change_attribute_base64_control() {
+	# create editor.sh
+	cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+computer_ldif="\$1"
+
+sed -i -e 's/displayName:: $display_name_con_b64/displayName: $display_name/' \
+	\$computer_ldif
+EOF
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool computer edit \
+		testmachine1 --editor=$tmpeditor \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_attribute_base64_control() {
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool computer show \
+		testmachine1 --attributes=displayName \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+# Test edit computer - change attributes with LDB_FLAG_FORCE_NO_BASE64_LDIF
+change_attribute_force_no_base64() {
+	# create editor.sh
+	# Expects that the original attribute is available as clear text,
+	# because the LDB_FLAG_FORCE_NO_BASE64_LDIF should be used here.
+	cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+computer_ldif="\$1"
+
+sed -i -e 's/displayName: $display_name/displayName: $display_name_new/' \
+	\$computer_ldif
+EOF
+
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool computer edit \
+		testmachine1 --editor=$tmpeditor \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_changed_attribute_force_no_base64() {
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool computer show \
+		 testmachine1 --attributes=displayName \
+		 -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+delete_computer() {
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool \
+		computer delete testmachine1 \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+failed=0
+
+testit "create_test_computer" create_test_computer || failed=`expr $failed + 1`
+testit "edit_computer" edit_computer || failed=`expr $failed + 1`
+testit "add_attribute_base64" add_attribute_base64 || failed=`expr $failed + 1`
+testit_grep "get_attribute_base64" "^displayName:: $display_name_b64" get_attribute_base64 || failed=`expr $failed + 1`
+testit "delete_attribute" delete_attribute || failed=`expr $failed + 1`
+testit "add_attribute_base64_control" add_attribute_base64_control || failed=`expr $failed + 1`
+testit_grep "get_attribute_base64_control" "^displayName:: $display_name_con_b64" get_attribute_base64_control || failed=`expr $failed + 1`
+testit "change_attribute_base64_control" change_attribute_base64_control || failed=`expr $failed + 1`
+testit_grep "get_attribute_base64_control" "^displayName:: $display_name_b64" get_attribute_base64_control || failed=`expr $failed + 1`
+testit "change_attribute_force_no_base64" change_attribute_force_no_base64 || failed=`expr $failed + 1`
+testit_grep "get_changed_attribute_force_no_base64" "^displayName: $display_name_new" get_changed_attribute_force_no_base64 || failed=`expr $failed + 1`
+testit "delete_computer" delete_computer || failed=`expr $failed + 1`
+
+rm -f $tmpeditor
+
+exit $failed
diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py
index b0c4ce581b3..ed4d695f78f 100755
--- a/source4/selftest/tests.py
+++ b/source4/selftest/tests.py
@@ -654,10 +654,11 @@ planpythontestsuite("none", "samba.tests.samba_tool.visualize", py3_compatible=T
 for env in all_fl_envs:
     planpythontestsuite(env + ":local", "samba.tests.samba_tool.fsmo", py3_compatible=True)
 
-# test user.edit
+# test samba-tool user and computer edit command
 for env in all_fl_envs:
     env += ":local"
     plantestsuite("samba.tests.samba_tool.user_edit", env, [os.path.join(srcdir(), "python/samba/tests/samba_tool/user_edit.sh"), '$SERVER', '$USERNAME', '$PASSWORD'])
+    plantestsuite("samba.tests.samba_tool.computer_edit", env, [os.path.join(srcdir(), "python/samba/tests/samba_tool/computer_edit.sh"), '$SERVER', '$USERNAME', '$PASSWORD'])
 
 # We run this test against both AD DC implementations because it is
 # the only test we have of GPO get/set behaviour, and this involves
-- 
2.19.2


From 5a6468ffe10e5f0e8cdbb426076a8f6a652d59db Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B6rn=20Baumbach?= <bb at sernet.de>
Date: Wed, 13 Mar 2019 21:20:29 +0100
Subject: [PATCH 15/18] samba-tool group: add 'edit' command to edit an AD
 group object
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Same like the samba-tool user edit command.

Signed-off-by: Björn Baumbach <bbaumbach at samba.org>
---
 python/samba/netcmd/group.py | 114 +++++++++++++++++++++++++++++++++++
 1 file changed, 114 insertions(+)

diff --git a/python/samba/netcmd/group.py b/python/samba/netcmd/group.py
index 6976f82d132..536c1cba613 100644
--- a/python/samba/netcmd/group.py
+++ b/python/samba/netcmd/group.py
@@ -35,6 +35,10 @@ from samba.dsdb import (
     GTYPE_DISTRIBUTION_UNIVERSAL_GROUP,
 )
 from collections import defaultdict
+from subprocess import check_call, CalledProcessError
+from samba.compat import get_bytes
+import os
+import tempfile
 
 security_group = dict({"Builtin": GTYPE_SECURITY_BUILTIN_LOCAL_GROUP,
                        "Domain": GTYPE_SECURITY_DOMAIN_LOCAL_GROUP,
@@ -691,12 +695,122 @@ class cmd_group_stats(Command):
         self.outf.write("\n* Note this does not include nested group memberships\n")
 
 
+class cmd_group_edit(Command):
+    """Modify Group AD object.
+
+    This command will allow editing of a group account in the Active Directory
+    domain. You will then be able to add or change attributes and their values.
+
+    The groupname specified on the command is the sAMAccountName.
+
+    The command may be run from the root userid or another authorized userid.
+
+    The -H or --URL= option can be used to execute the command against a remote
+    server.
+
+    Example1:
+    samba-tool group edit Group1 -H ldap://samba.samdom.example.com \\
+        -U administrator --password=passw1rd
+
+    Example1 shows how to edit a groups attributes in the domain against a
+    remote LDAP server.
+
+    The -H parameter is used to specify the remote target server.
+
+    Example2:
+    samba-tool group edit Group2
+
+    Example2 shows how to edit a groups attributes in the domain against a local
+    server.
+
+    Example3:
+    samba-tool group edit Group3 --editor=nano
+
+    Example3 shows how to edit a groups attributes in the domain against a local
+    server using the 'nano' editor.
+    """
+    synopsis = "%prog <groupname> [options]"
+
+    takes_options = [
+        Option("-H", "--URL", help="LDB URL for database or target server",
+               type=str, metavar="URL", dest="H"),
+        Option("--editor", help="Editor to use instead of the system default,"
+               " or 'vi' if no system default is set.", type=str),
+    ]
+
+    takes_args = ["groupname"]
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "credopts": options.CredentialsOptions,
+        "versionopts": options.VersionOptions,
+    }
+
+    def run(self, groupname, credopts=None, sambaopts=None, versionopts=None,
+            H=None, editor=None):
+        from . import common
+
+        lp = sambaopts.get_loadparm()
+        creds = credopts.get_credentials(lp, fallback_machine=True)
+        samdb = SamDB(url=H, session_info=system_session(),
+                      credentials=creds, lp=lp)
+
+        filter = ("(&(sAMAccountName=%s)(objectClass=group))" % groupname)
+
+        domaindn = samdb.domain_dn()
+
+        try:
+            res = samdb.search(base=domaindn,
+                               expression=filter,
+                               scope=ldb.SCOPE_SUBTREE)
+            group_dn = res[0].dn
+        except IndexError:
+            raise CommandError('Unable to find group "%s"' % (groupname))
+
+        if len(res) != 1:
+            raise CommandError('Invalid number of results: for "%s": %d' %
+                               ((groupname), len(res)))
+
+        msg = res[0]
+        result_ldif = common.get_ldif_for_editor(samdb, msg)
+
+        if editor is None:
+            editor = os.environ.get('EDITOR')
+            if editor is None:
+                editor = 'vi'
+
+        with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
+            t_file.write(get_bytes(result_ldif))
+            t_file.flush()
+            try:
+                check_call([editor, t_file.name])
+            except CalledProcessError as e:
+                raise CalledProcessError("ERROR: ", e)
+            with open(t_file.name) as edited_file:
+                edited_message = edited_file.read()
+
+        msgs_edited = samdb.parse_ldif(edited_message)
+        msg_edited = next(msgs_edited)[1]
+
+        res_msg_diff = samdb.msg_diff(msg, msg_edited)
+        if len(res_msg_diff) == 0:
+            self.outf.write("Nothing to do\n")
+            return
+
+        try:
+            samdb.modify(res_msg_diff)
+        except Exception as e:
+            raise CommandError("Failed to modify group '%s': " % groupname, e)
+
+        self.outf.write("Modified group '%s' successfully\n" % groupname)
+
+
 class cmd_group(SuperCommand):
     """Group management."""
 
     subcommands = {}
     subcommands["add"] = cmd_group_add()
     subcommands["delete"] = cmd_group_delete()
+    subcommands["edit"] = cmd_group_edit()
     subcommands["addmembers"] = cmd_group_add_members()
     subcommands["removemembers"] = cmd_group_remove_members()
     subcommands["list"] = cmd_group_list()
-- 
2.19.2


From fae029ed7fa0c8ff2fb55c3cd94a6d164793bfca Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B6rn=20Baumbach?= <bb at sernet.de>
Date: Mon, 18 Mar 2019 13:31:04 +0100
Subject: [PATCH 16/18] samba-tool tests: add test for 'samba-tool group edit'
 command
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Björn Baumbach <bbaumbach at samba.org>
---
 python/samba/tests/samba_tool/group_edit.sh | 208 ++++++++++++++++++++
 source4/selftest/tests.py                   |   3 +-
 2 files changed, 210 insertions(+), 1 deletion(-)
 create mode 100755 python/samba/tests/samba_tool/group_edit.sh

diff --git a/python/samba/tests/samba_tool/group_edit.sh b/python/samba/tests/samba_tool/group_edit.sh
new file mode 100755
index 00000000000..90f5252d926
--- /dev/null
+++ b/python/samba/tests/samba_tool/group_edit.sh
@@ -0,0 +1,208 @@
+#!/bin/sh
+#
+# Test for 'samba-tool group edit'
+
+if [ $# -lt 3 ]; then
+cat <<EOF
+Usage: group_edit.sh SERVER USERNAME PASSWORD
+EOF
+exit 1;
+fi
+
+SERVER="$1"
+USERNAME="$2"
+PASSWORD="$3"
+
+STpath=$(pwd)
+. $STpath/testprogs/blackbox/subunit.sh
+
+display_name="Users in Göttingen"
+display_name_b64="VXNlcnMgaW4gR8O2dHRpbmdlbg=="
+display_name_new="Users in Goettingen"
+# attribute value including control character
+# echo -e "test \a string" | base64
+display_name_con_b64="dGVzdCAHIHN0cmluZwo="
+
+tmpeditor=$(mktemp --suffix .sh -p $STpath/bin samba-tool-editor-XXXXXXXX)
+chmod +x $tmpeditor
+
+create_test_group() {
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool \
+		group add testgroup1 \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+delete_test_group() {
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool \
+		group delete testgroup1 \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+create_test_user() {
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool \
+		user create testuser1 --random-password \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+delete_test_user() {
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool \
+		user delete testuser1 \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+add_member() {
+	user_dn=$($PYTHON ${STpath}/source4/scripting/bin/samba-tool \
+		user show testuser1 --attributes=dn \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD" | \
+		grep ^dn: | cut -d' ' -f2)
+
+	# create editor.sh
+	cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+group_ldif="\$1"
+
+grep -v '^$' \$group_ldif > \${group_ldif}.tmp
+echo "member: $user_dn" >> \${group_ldif}.tmp
+
+mv \${group_ldif}.tmp \$group_ldif
+EOF
+
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool \
+		group edit testgroup1 --editor=$tmpeditor \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_member() {
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool \
+		group listmembers testgroup1 \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+# Test edit group - add base64 attributes
+add_attribute_base64() {
+	# create editor.sh
+	cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+group_ldif="\$1"
+
+grep -v '^$' \$group_ldif > \${group_ldif}.tmp
+echo "displayName:: $display_name_b64" >> \${group_ldif}.tmp
+
+mv \${group_ldif}.tmp \$group_ldif
+EOF
+
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool group edit \
+		testgroup1 --editor=$tmpeditor \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_attribute_base64() {
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool group show \
+		testgroup1 --attributes=displayName \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+delete_attribute() {
+	# create editor.sh
+	cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+group_ldif="\$1"
+
+grep -v '^displayName' \$group_ldif >> \${group_ldif}.tmp
+mv \${group_ldif}.tmp \$group_ldif
+EOF
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool group edit \
+		testgroup1 --editor=$tmpeditor \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+# Test edit group - add base64 attribute value including control character
+add_attribute_base64_control() {
+	# create editor.sh
+	cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+group_ldif="\$1"
+
+grep -v '^$' \$group_ldif > \${group_ldif}.tmp
+echo "displayName:: $display_name_con_b64" >> \${group_ldif}.tmp
+
+mv \${group_ldif}.tmp \$group_ldif
+EOF
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool group edit \
+		testgroup1 --editor=$tmpeditor \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_attribute_base64_control() {
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool group show \
+		testgroup1 --attributes=displayName \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+
+# Test edit group - change base64 attribute value including control character
+change_attribute_base64_control() {
+	# create editor.sh
+	cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+group_ldif="\$1"
+
+sed -i -e 's/displayName:: $display_name_con_b64/displayName: $display_name/' \
+	\$group_ldif
+EOF
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool group edit \
+		testgroup1 --editor=$tmpeditor \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_attribute_base64_control() {
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool group show \
+		testgroup1 --attributes=displayName \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+# Test edit group - change attributes with LDB_FLAG_FORCE_NO_BASE64_LDIF
+change_attribute_force_no_base64() {
+	# create editor.sh
+	# Expects that the original attribute is available as clear text,
+	# because the LDB_FLAG_FORCE_NO_BASE64_LDIF should be used here.
+	cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+group_ldif="\$1"
+
+sed -i -e 's/displayName: $display_name/displayName: $display_name_new/' \
+	\$group_ldif
+EOF
+
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool group edit \
+		testgroup1 --editor=$tmpeditor \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_changed_attribute_force_no_base64() {
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool group show \
+		 testgroup1 --attributes=displayName \
+		 -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+failed=0
+
+testit "create_test_group" create_test_group || failed=`expr $failed + 1`
+testit "create_test_user" create_test_user || failed=`expr $failed + 1`
+testit "add_member" add_member || failed=`expr $failed + 1`
+testit_grep "get_member" "^testuser1" get_member || failed=`expr $failed + 1`
+testit "add_attribute_base64" add_attribute_base64 || failed=`expr $failed + 1`
+testit_grep "get_attribute_base64" "^displayName:: $display_name_b64" get_attribute_base64 || failed=`expr $failed + 1`
+testit "delete_attribute" delete_attribute || failed=`expr $failed + 1`
+testit "add_attribute_base64_control" add_attribute_base64_control || failed=`expr $failed + 1`
+testit_grep "get_attribute_base64_control" "^displayName:: $display_name_con_b64" get_attribute_base64_control || failed=`expr $failed + 1`
+testit "change_attribute_base64_control" change_attribute_base64_control || failed=`expr $failed + 1`
+testit_grep "get_attribute_base64_control" "^displayName:: $display_name_b64" get_attribute_base64_control || failed=`expr $failed + 1`
+testit "change_attribute_force_no_base64" change_attribute_force_no_base64 || failed=`expr $failed + 1`
+testit_grep "get_changed_attribute_force_no_base64" "^displayName: $display_name_new" get_changed_attribute_force_no_base64 || failed=`expr $failed + 1`
+testit "delete_test_group" delete_test_group || failed=`expr $failed + 1`
+testit "delete_test_user" delete_test_user || failed=`expr $failed + 1`
+
+rm -f $tmpeditor
+
+exit $failed
diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py
index ed4d695f78f..cc2410537b4 100755
--- a/source4/selftest/tests.py
+++ b/source4/selftest/tests.py
@@ -654,10 +654,11 @@ planpythontestsuite("none", "samba.tests.samba_tool.visualize", py3_compatible=T
 for env in all_fl_envs:
     planpythontestsuite(env + ":local", "samba.tests.samba_tool.fsmo", py3_compatible=True)
 
-# test samba-tool user and computer edit command
+# test samba-tool user, group and computer edit command
 for env in all_fl_envs:
     env += ":local"
     plantestsuite("samba.tests.samba_tool.user_edit", env, [os.path.join(srcdir(), "python/samba/tests/samba_tool/user_edit.sh"), '$SERVER', '$USERNAME', '$PASSWORD'])
+    plantestsuite("samba.tests.samba_tool.group_edit", env, [os.path.join(srcdir(), "python/samba/tests/samba_tool/group_edit.sh"), '$SERVER', '$USERNAME', '$PASSWORD'])
     plantestsuite("samba.tests.samba_tool.computer_edit", env, [os.path.join(srcdir(), "python/samba/tests/samba_tool/computer_edit.sh"), '$SERVER', '$USERNAME', '$PASSWORD'])
 
 # We run this test against both AD DC implementations because it is
-- 
2.19.2


From 04282aebfd8850872cf2f7fa56e58bc596f99c47 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B6rn=20Baumbach?= <bb at sernet.de>
Date: Tue, 19 Mar 2019 17:55:37 +0100
Subject: [PATCH 17/18] samba-tool: implement contact management commands
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Usage: samba-tool contact <subcommand>

Contact management.

Available subcommands:
  create  - Create a new contact.
  delete  - Delete a contact.
  edit    - Modify a contact.
  list    - List all contacts.
  move    - Move a contact object to an organizational unit or container.
  show    - Display a contact.

Signed-off-by: Björn Baumbach <bbaumbach at samba.org>
---
 python/samba/netcmd/contact.py | 687 +++++++++++++++++++++++++++++++++
 python/samba/netcmd/main.py    |   1 +
 python/samba/samdb.py          | 108 ++++++
 3 files changed, 796 insertions(+)
 create mode 100644 python/samba/netcmd/contact.py

diff --git a/python/samba/netcmd/contact.py b/python/samba/netcmd/contact.py
new file mode 100644
index 00000000000..9ed2f9f8d2c
--- /dev/null
+++ b/python/samba/netcmd/contact.py
@@ -0,0 +1,687 @@
+# samba-tool contact management
+#
+# Copyright Björn Baumbach 2019 <bbaumbach at 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 ldb
+import os
+import tempfile
+from subprocess import check_call, CalledProcessError
+from operator import attrgetter
+from samba.auth import system_session
+from samba.samdb import SamDB
+from samba import (
+    credentials,
+    dsdb,
+)
+from samba.net import Net
+
+from samba.netcmd import (
+    Command,
+    CommandError,
+    SuperCommand,
+    Option,
+)
+from samba.compat import get_bytes
+
+
+class cmd_create(Command):
+    """Create a new contact.
+
+    This command creates a new contact in the Active Directory domain.
+
+    The name of the new contact can be specified by the first argument
+    'contactname' or the --given-name, --initial and --surname arugments.
+    If no 'contactname' is given, contact's name will be made up of the given
+    arguments by combining the given-name, initials and surname. Each argument
+    is optional. A dot ('.') will be appended to the initials automatically.
+
+    Example1:
+    samba-tool contact create "James T. Kirk" --job-title=Captain \\
+        -H ldap://samba.samdom.example.com -UAdministrator%Passw1rd
+
+    The example shows how to create a new contact in the domain against a remote
+    LDAP server.
+
+    Example2:
+    samba-tool contact create --given-name=James --initials=T --surname=Kirk
+
+    The example shows how to create a new contact in the domain against a local
+    server. The resulting name is "James T. Kirk".
+    """
+
+    synopsis = "%prog [contactname] [options]"
+
+    takes_options = [
+        Option("-H", "--URL", help="LDB URL for database or target server",
+               type=str, metavar="URL", dest="H"),
+        Option("--ou",
+               help=("DN of alternative location (with or without domainDN "
+                     "counterpart) in which the new contact will be created."
+                     "E.g. 'OU=<OU name>'."
+                     "Default is the domain base."),
+               type=str),
+        Option("--surname", help="Contact's surname", type=str),
+        Option("--given-name", help="Contact's given name", type=str),
+        Option("--initials", help="Contact's initials", type=str),
+        Option("--display-name", help="Contact's display name", type=str),
+        Option("--job-title", help="Contact's job title", type=str),
+        Option("--department", help="Contact's department", type=str),
+        Option("--company", help="Contact's company", type=str),
+        Option("--description", help="Contact's description", type=str),
+        Option("--mail-address", help="Contact's email address", type=str),
+        Option("--internet-address", help="Contact's home page", type=str),
+        Option("--telephone-number", help="Contact's phone number", type=str),
+        Option("--mobile-number",
+               help="Contact's mobile phone number",
+               type=str),
+        Option("--physical-delivery-office",
+               help="Contact's office location",
+               type=str),
+    ]
+
+    takes_args = ["fullcontactname?"]
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "credopts": options.CredentialsOptions,
+        "versionopts": options.VersionOptions,
+    }
+
+    def run(self,
+            fullcontactname=None,
+            sambaopts=None,
+            credopts=None,
+            versionopts=None,
+            H=None,
+            ou=None,
+            surname=None,
+            given_name=None,
+            initials=None,
+            display_name=None,
+            job_title=None,
+            department=None,
+            company=None,
+            description=None,
+            mail_address=None,
+            internet_address=None,
+            telephone_number=None,
+            mobile_number=None,
+            physical_delivery_office=None):
+
+        lp = sambaopts.get_loadparm()
+        creds = credopts.get_credentials(lp)
+
+        try:
+            samdb = SamDB(url=H,
+                          session_info=system_session(),
+                          credentials=creds,
+                          lp=lp)
+            ret_name = samdb.newcontact(
+                fullcontactname=fullcontactname,
+                ou=ou,
+                surname=surname,
+                givenname=given_name,
+                initials=initials,
+                displayname=display_name,
+                jobtitle=job_title,
+                department=department,
+                company=company,
+                description=description,
+                mailaddress=mail_address,
+                internetaddress=internet_address,
+                telephonenumber=telephone_number,
+                mobilenumber=mobile_number,
+                physicaldeliveryoffice=physical_delivery_office)
+        except Exception as e:
+            raise CommandError("Failed to create contact", e)
+
+        self.outf.write("Contact '%s' created successfully\n" % ret_name)
+
+
+class cmd_delete(Command):
+    """Delete a contact.
+
+    This command deletes a contact object from the Active Directory domain.
+
+    The contactname specified on the command is the display name, common name or
+    the distinguished name of the contact object. The distinguished name of the
+    contact can be specified with or without the domainDN component.
+
+    Example:
+    samba-tool contact delete Contact1 \\
+        -H ldap://samba.samdom.example.com \\
+        --username=Administrator --password=Passw1rd
+
+    The example shows how to delete a contact in the domain against a remote
+    LDAP server.
+    """
+    synopsis = "%prog <contactname> [options]"
+
+    takes_options = [
+        Option("-H",
+               "--URL",
+               help="LDB URL for database or target server",
+               type=str,
+               metavar="URL",
+               dest="H"),
+    ]
+
+    takes_args = ["contactname"]
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "credopts": options.CredentialsOptions,
+        "versionopts": options.VersionOptions,
+    }
+
+    def run(self,
+            contactname,
+            sambaopts=None,
+            credopts=None,
+            versionopts=None,
+            H=None):
+        lp = sambaopts.get_loadparm()
+        creds = credopts.get_credentials(lp, fallback_machine=True)
+        samdb = SamDB(url=H,
+                      session_info=system_session(),
+                      credentials=creds,
+                      lp=lp)
+        base_dn = samdb.domain_dn()
+        scope = ldb.SCOPE_SUBTREE
+
+        filter = ("(&(objectClass=contact)(|(name=%s)(displayName=%s)))" %
+                  (ldb.binary_encode(contactname),
+                   ldb.binary_encode(contactname)))
+
+        if contactname.upper().startswith("CN="):
+            # contact is specified by DN
+            filter = "(objectClass=contact)"
+            scope = ldb.SCOPE_BASE
+            try:
+                base_dn = samdb.normalize_dn_in_domain(contactname)
+            except Exception as e:
+                raise CommandError('Invalid dn "%s": %s' %
+                                   (contactname, e))
+
+        try:
+            res = samdb.search(base=base_dn,
+                               scope=scope,
+                               expression=filter,
+                               attrs=["dn"])
+            contact_dn = res[0].dn
+        except IndexError:
+            raise CommandError('Unable to find contact "%s"' % (contactname))
+
+        if len(res) > 1:
+            for msg in sorted(res, key=attrgetter('dn')):
+                self.outf.write("found: %s\n" % msg.dn)
+            raise CommandError("Multiple results for contact '%s'\n"
+                               "Please specify the contact's full DN" %
+                               contactname)
+
+        try:
+            samdb.delete(contact_dn)
+        except Exception as e:
+            raise CommandError('Failed to remove contact "%s"' % contactname, e)
+        self.outf.write("Deleted contact %s\n" % contactname)
+
+
+class cmd_list(Command):
+    """List all contacts.
+
+    If a contact has a displayName specified, the displayName will be used.
+    Otherwise the common name will be used for listing.
+    """
+
+    synopsis = "%prog [options]"
+
+    takes_options = [
+        Option("-H",
+               "--URL",
+               help="LDB URL for database or target server",
+               type=str,
+               metavar="URL",
+               dest="H"),
+        Option("--full-dn",
+               dest="full_dn",
+               default=False,
+               action='store_true',
+               help="Display contact's full DN instead of the name."),
+    ]
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "credopts": options.CredentialsOptions,
+        "versionopts": options.VersionOptions,
+    }
+
+    def run(self,
+            sambaopts=None,
+            credopts=None,
+            versionopts=None,
+            H=None,
+            full_dn=False):
+        lp = sambaopts.get_loadparm()
+        creds = credopts.get_credentials(lp, fallback_machine=True)
+
+        samdb = SamDB(url=H,
+                      session_info=system_session(),
+                      credentials=creds,
+                      lp=lp)
+
+        domain_dn = samdb.domain_dn()
+        res = samdb.search(domain_dn,
+                           scope=ldb.SCOPE_SUBTREE,
+                           expression="(objectClass=contact)",
+                           attrs=["name",
+                                  "displayName"])
+        if (len(res) == 0):
+            return
+
+        if full_dn:
+            for msg in sorted(res, key=attrgetter('dn')):
+                self.outf.write("%s\n" % msg.dn)
+            return
+
+        for msg in res:
+            contact_str = msg.get("name", idx=0)
+            display_name = msg.get("displayName", idx=0)
+            if display_name:
+                contact_str = display_name
+
+            self.outf.write("%s\n" % contact_str)
+
+
+class cmd_edit(Command):
+    """Modify a contact.
+
+    This command will allow editing of a contact object in the Active Directory
+    domain. You will then be able to add or change attributes and their values.
+
+    The contactname specified on the command is the display name, common name or
+    the distinguished name of the contact object. The distinguished name of the
+    contact can be specified with or without the domainDN component.
+
+    The command may be run from the root userid or another authorized userid.
+
+    The -H or --URL= option can be used to execute the command against a remote
+    server.
+
+    Example1:
+    samba-tool contact edit Contact1 -H ldap://samba.samdom.example.com \\
+        -U Administrator --password=Passw1rd
+
+    Example1 shows how to edit a contact's attributes in the domain against a
+    remote LDAP server.
+
+    The -H parameter is used to specify the remote target server.
+
+    Example2:
+    samba-tool contact edit CN=Contact2,OU=people,DC=samdom,DC=example,DC=com
+
+    Example2 shows how to edit a contact's attributes in the domain against a
+    local server. The contact, which is located in the 'people' OU,
+    is specified by the full distinguished name.
+
+    Example3:
+    samba-tool contact edit Contact3 --editor=nano
+
+    Example3 shows how to edit a contact's attributes in the domain against a
+    local server using the 'nano' editor.
+    """
+    synopsis = "%prog <contactname> [options]"
+
+    takes_options = [
+        Option("-H",
+               "--URL",
+               help="LDB URL for database or target server",
+               type=str,
+               metavar="URL",
+               dest="H"),
+        Option("--editor",
+               help="Editor to use instead of the system default, "
+                    "or 'vi' if no system default is set.",
+               type=str),
+    ]
+
+    takes_args = ["contactname"]
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "credopts": options.CredentialsOptions,
+        "versionopts": options.VersionOptions,
+    }
+
+    def run(self,
+            contactname,
+            sambaopts=None,
+            credopts=None,
+            versionopts=None,
+            H=None,
+            editor=None):
+        from . import common
+
+        lp = sambaopts.get_loadparm()
+        creds = credopts.get_credentials(lp, fallback_machine=True)
+        samdb = SamDB(url=H, session_info=system_session(),
+                      credentials=creds, lp=lp)
+        base_dn = samdb.domain_dn()
+        scope = ldb.SCOPE_SUBTREE
+
+        filter = ("(&(objectClass=contact)(|(name=%s)(displayName=%s)))" %
+                  (ldb.binary_encode(contactname),
+                   ldb.binary_encode(contactname)))
+
+        if contactname.upper().startswith("CN="):
+            # contact is specified by DN
+            filter = "(objectClass=contact)"
+            scope = ldb.SCOPE_BASE
+            try:
+                base_dn = samdb.normalize_dn_in_domain(contactname)
+            except Exception as e:
+                raise CommandError('Invalid dn "%s": %s' %
+                                   (contactname, e))
+
+        try:
+            res = samdb.search(base=base_dn,
+                               scope=scope,
+                               expression=filter)
+            contact_dn = res[0].dn
+        except IndexError:
+            raise CommandError('Unable to find contact "%s"' % (contactname))
+
+        if len(res) > 1:
+            for msg in sorted(res, key=attrgetter('dn')):
+                self.outf.write("found: %s\n" % msg.dn)
+            raise CommandError("Multiple results for contact '%s'\n"
+                               "Please specify the contact's full DN" %
+                               contactname)
+
+        for msg in res:
+            result_ldif = common.get_ldif_for_editor(samdb, msg)
+
+            if editor is None:
+                editor = os.environ.get('EDITOR')
+                if editor is None:
+                    editor = 'vi'
+
+            with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
+                t_file.write(get_bytes(result_ldif))
+                t_file.flush()
+                try:
+                    check_call([editor, t_file.name])
+                except CalledProcessError as e:
+                    raise CalledProcessError("ERROR: ", e)
+                with open(t_file.name) as edited_file:
+                    edited_message = edited_file.read()
+
+
+        msgs_edited = samdb.parse_ldif(edited_message)
+        msg_edited = next(msgs_edited)[1]
+
+        res_msg_diff = samdb.msg_diff(msg, msg_edited)
+        if len(res_msg_diff) == 0:
+            self.outf.write("Nothing to do\n")
+            return
+
+        try:
+            samdb.modify(res_msg_diff)
+        except Exception as e:
+            raise CommandError("Failed to modify contact '%s': " % contactname,
+                               e)
+
+        self.outf.write("Modified contact '%s' successfully\n" % contactname)
+
+
+class cmd_show(Command):
+    """Display a contact.
+
+    This command displays a contact object with it's attributes in the Active
+    Directory domain.
+
+    The contactname specified on the command is the display name, common name or
+    the distinguished name of the contact object. The distinguished name of the
+    contact can be specified with or without the domainDN component.
+
+    The command may be run from the root userid or another authorized userid.
+
+    The -H or --URL= option can be used to execute the command against a remote
+    server.
+
+    Example1:
+    samba-tool contact show Contact1 -H ldap://samba.samdom.example.com \\
+        -U Administrator --password=Passw1rd
+
+    Example1 shows how to display a contact's attributes in the domain against
+    a remote LDAP server.
+
+    The -H parameter is used to specify the remote target server.
+
+    Example2:
+    samba-tool contact show CN=Contact2,OU=people,DC=samdom,DC=example,DC=com
+
+    Example2 shows how to display a contact's attributes in the domain against
+    a local server. The contact, which is located in the 'people' OU, is
+    specified by the full distinguished name.
+
+    Example3:
+    samba-tool contact show Contact3 --attributes=mail,mobile
+
+    Example3 shows how to display a contact's mail and mobile attributes.
+    """
+    synopsis = "%prog <contactname> [options]"
+
+    takes_options = [
+        Option("-H",
+               "--URL",
+               help="LDB URL for database or target server",
+               type=str,
+               metavar="URL",
+               dest="H"),
+        Option("--attributes",
+               help=("Comma separated list of attributes, "
+                     "which will be printed."),
+               type=str,
+               dest="contact_attrs"),
+    ]
+
+    takes_args = ["contactname"]
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "credopts": options.CredentialsOptions,
+        "versionopts": options.VersionOptions,
+    }
+
+    def run(self,
+            contactname,
+            sambaopts=None,
+            credopts=None,
+            versionopts=None,
+            H=None,
+            contact_attrs=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)
+        base_dn = samdb.domain_dn()
+        scope = ldb.SCOPE_SUBTREE
+
+        attrs = None
+        if contact_attrs:
+            attrs = contact_attrs.split(",")
+
+        filter = ("(&(objectClass=contact)(|(name=%s)(displayName=%s)))" %
+                  (ldb.binary_encode(contactname),
+                   ldb.binary_encode(contactname)))
+
+        if contactname.upper().startswith("CN="):
+            # contact is specified by DN
+            filter = "(objectClass=contact)"
+            scope = ldb.SCOPE_BASE
+            try:
+                base_dn = samdb.normalize_dn_in_domain(contactname)
+            except Exception as e:
+                raise CommandError('Invalid dn "%s": %s' %
+                                   (contactname, e))
+
+        try:
+            res = samdb.search(base=base_dn,
+                               expression=filter,
+                               scope=scope,
+                               attrs=attrs)
+            contact_dn = res[0].dn
+        except IndexError:
+            raise CommandError('Unable to find contact "%s"' % (contactname))
+
+        if len(res) > 1:
+            for msg in sorted(res, key=attrgetter('dn')):
+                self.outf.write("found: %s\n" % msg.dn)
+            raise CommandError("Multiple results for contact '%s'\n"
+                               "Please specify the contact's DN" %
+                               contactname)
+
+        for msg in res:
+            contact_ldif = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
+            self.outf.write(contact_ldif)
+
+
+class cmd_move(Command):
+    """Move a contact object to an organizational unit or container.
+
+    The contactname specified on the command is the display name, common name or
+    the distinguished name of the contact object. The distinguished name of the
+    contact can be specified with or without the domainDN component.
+
+    The name of the organizational unit or container can be specified as the
+    distinguished name, with or without the domainDN component.
+
+    The command may be run from the root userid or another authorized userid.
+
+    The -H or --URL= option can be used to execute the command against a remote
+    server.
+
+    Example1:
+    samba-tool contact move Contact1 'OU=people' \\
+        -H ldap://samba.samdom.example.com -U Administrator
+
+    Example1 shows how to move a contact Contact1 into the 'people'
+    organizational unit on a remote LDAP server.
+
+    The -H parameter is used to specify the remote target server.
+
+    Example2:
+    samba-tool contact move Contact1 OU=Contacts,DC=samdom,DC=example,DC=com
+
+    Example2 shows how to move a contact Contact1 into the OU=Contacts
+    organizational unit on the local server.
+    """
+
+    synopsis = "%prog <contactname> <new_parent_dn> [options]"
+
+    takes_options = [
+        Option("-H",
+               "--URL",
+               help="LDB URL for database or target server",
+               type=str,
+               metavar="URL",
+               dest="H"),
+    ]
+
+    takes_args = ["contactname", "new_parent_dn"]
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "credopts": options.CredentialsOptions,
+        "versionopts": options.VersionOptions,
+    }
+
+    def run(self,
+            contactname,
+            new_parent_dn,
+            sambaopts=None,
+            credopts=None,
+            versionopts=None,
+            H=None):
+        lp = sambaopts.get_loadparm()
+        creds = credopts.get_credentials(lp, fallback_machine=True)
+        samdb = SamDB(url=H,
+                      session_info=system_session(),
+                      credentials=creds,
+                      lp=lp)
+        base_dn = samdb.domain_dn()
+        scope = ldb.SCOPE_SUBTREE
+
+        filter = ("(&(objectClass=contact)(|(name=%s)(displayName=%s)))" %
+                  (ldb.binary_encode(contactname),
+                   ldb.binary_encode(contactname)))
+
+        if contactname.upper().startswith("CN="):
+            # contact is specified by DN
+            filter = "(objectClass=contact)"
+            scope = ldb.SCOPE_BASE
+            try:
+                base_dn = samdb.normalize_dn_in_domain(contactname)
+            except Exception as e:
+                raise CommandError('Invalid dn "%s": %s' %
+                                   (contactname, e))
+
+        try:
+            res = samdb.search(base=base_dn,
+                               scope=scope,
+                               expression=filter,
+                               attrs=["dn"])
+            contact_dn = res[0].dn
+        except IndexError:
+            raise CommandError('Unable to find contact "%s"' % (contactname))
+
+        if len(res) > 1:
+            for msg in sorted(res, key=attrgetter('dn')):
+                self.outf.write("found: %s\n" % msg.dn)
+            raise CommandError("Multiple results for contact '%s'\n"
+                               "Please specify the contact's full DN" %
+                               contactname)
+
+        try:
+            full_new_parent_dn = samdb.normalize_dn_in_domain(new_parent_dn)
+        except Exception as e:
+            raise CommandError('Invalid new_parent_dn "%s": %s' %
+                               (new_parent_dn, e))
+
+        full_new_contact_dn = ldb.Dn(samdb, str(contact_dn))
+        full_new_contact_dn.remove_base_components(len(contact_dn) - 1)
+        full_new_contact_dn.add_base(full_new_parent_dn)
+
+        try:
+            samdb.rename(contact_dn, full_new_contact_dn)
+        except Exception as e:
+            raise CommandError('Failed to move contact "%s"' % contactname, e)
+        self.outf.write('Moved contact "%s" into "%s"\n' %
+                        (contactname, full_new_parent_dn))
+
+
+class cmd_contact(SuperCommand):
+    """Contact management."""
+
+    subcommands = {}
+    subcommands["create"] = cmd_create()
+    subcommands["delete"] = cmd_delete()
+    subcommands["edit"] = cmd_edit()
+    subcommands["list"] = cmd_list()
+    subcommands["move"] = cmd_move()
+    subcommands["show"] = cmd_show()
diff --git a/python/samba/netcmd/main.py b/python/samba/netcmd/main.py
index 261cb78163d..88c33c5aa1a 100644
--- a/python/samba/netcmd/main.py
+++ b/python/samba/netcmd/main.py
@@ -58,6 +58,7 @@ class cmd_sambatool(SuperCommand):
     subcommands = cache_loader()
 
     subcommands["computer"] = None
+    subcommands["contact"] = None
     subcommands["dbcheck"] = None
     subcommands["delegation"] = None
     subcommands["dns"] = None
diff --git a/python/samba/samdb.py b/python/samba/samdb.py
index 308b5f96a7b..278d7a0ca74 100644
--- a/python/samba/samdb.py
+++ b/python/samba/samdb.py
@@ -497,6 +497,114 @@ member: %s
         else:
             self.transaction_commit()
 
+    def newcontact(self,
+                   fullcontactname=None,
+                   ou=None,
+                   surname=None,
+                   givenname=None,
+                   initials=None,
+                   displayname=None,
+                   jobtitle=None,
+                   department=None,
+                   company=None,
+                   description=None,
+                   mailaddress=None,
+                   internetaddress=None,
+                   telephonenumber=None,
+                   mobilenumber=None,
+                   physicaldeliveryoffice=None):
+        """Adds a new contact with additional parameters
+
+        :param fullcontactname: Optional full name of the new contact
+        :param ou: Object container for new contact
+        :param surname: Surname of the new contact
+        :param givenname: First name of the new contact
+        :param initials: Initials of the new contact
+        :param displayname: displayName of the new contact
+        :param jobtitle: Job title of the new contact
+        :param department: Department of the new contact
+        :param company: Company of the new contact
+        :param description: Description of the new contact
+        :param mailaddress: Email address of the new contact
+        :param internetaddress: Home page of the new contact
+        :param telephonenumber: Phone number of the new contact
+        :param mobilenumber: Primary mobile number of the new contact
+        :param physicaldeliveryoffice: Office location of the new contact
+        """
+
+        # Prepare the contact name like the RSAT, using the name parts.
+        cn = ""
+        if givenname is not None:
+            cn += givenname
+
+        if initials is not None:
+            cn += ' %s.' % initials
+
+        if surname is not None:
+            cn += ' %s' % surname
+
+        # Use the specified fullcontactname instead of the previously prepared
+        # contact name, if it is specified.
+        # This is similar to the "Full name" value of the RSAT.
+        if fullcontactname is not None:
+            cn = fullcontactname
+
+        if fullcontactname is None and cn == "":
+            raise Exception('No name for contact specified')
+
+        contactcontainer_dn = self.domain_dn()
+        if ou:
+            contactcontainer_dn = self.normalize_dn_in_domain(ou)
+
+        contact_dn = "CN=%s,%s" % (cn, contactcontainer_dn)
+
+        ldbmessage = {"dn": contact_dn,
+                      "objectClass": "contact",
+                      }
+
+        if surname is not None:
+            ldbmessage["sn"] = surname
+
+        if givenname is not None:
+            ldbmessage["givenName"] = givenname
+
+        if displayname is not None:
+            ldbmessage["displayName"] = displayname
+
+        if initials is not None:
+            ldbmessage["initials"] = '%s.' % initials
+
+        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 mobilenumber is not None:
+            ldbmessage["mobile"] = mobilenumber
+
+        if physicaldeliveryoffice is not None:
+            ldbmessage["physicalDeliveryOfficeName"] = physicaldeliveryoffice
+
+        self.add(ldbmessage)
+
+        return cn
+
     def newcomputer(self, computername, computerou=None, description=None,
                     prepare_oldjoin=False, ip_address_list=None,
                     service_principal_name_list=None):
-- 
2.19.2


From e2ed8f2466fe88110e49bedb07fd876dd620a87d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B6rn=20Baumbach?= <bb at sernet.de>
Date: Wed, 20 Mar 2019 17:17:05 +0100
Subject: [PATCH 18/18] samba-tool tests: add tests for contact management
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Björn Baumbach <bbaumbach at samba.org>
---
 python/samba/tests/samba_tool/contact.py      | 321 ++++++++++++++++++
 python/samba/tests/samba_tool/contact_edit.sh | 164 +++++++++
 source4/selftest/tests.py                     |   4 +-
 3 files changed, 488 insertions(+), 1 deletion(-)
 create mode 100644 python/samba/tests/samba_tool/contact.py
 create mode 100755 python/samba/tests/samba_tool/contact_edit.sh

diff --git a/python/samba/tests/samba_tool/contact.py b/python/samba/tests/samba_tool/contact.py
new file mode 100644
index 00000000000..0abf325e1f1
--- /dev/null
+++ b/python/samba/tests/samba_tool/contact.py
@@ -0,0 +1,321 @@
+# Unix SMB/CIFS implementation.
+#
+# Tests for samba-tool contact management commands
+#
+# Copyright (C) Björn Baumbach <bbaumbach at samba.org> 2019
+#
+# 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 ldb
+from samba.tests.samba_tool.base import SambaToolCmdTest
+
+class ContactCmdTestCase(SambaToolCmdTest):
+    """Tests for samba-tool contact subcommands"""
+    contacts = []
+    samdb = None
+
+    def setUp(self):
+        super(ContactCmdTestCase, self).setUp()
+        self.creds = "-U%s%%%s" % (os.environ["DC_USERNAME"],
+                                   os.environ["DC_PASSWORD"])
+        self.samdb = self.getSamDB("-H",
+                                   "ldap://%s" % os.environ["DC_SERVER"],
+                                   self.creds)
+        contact = None
+        self.contacts = []
+
+        contact = self._randomContact({"expectedname": "contact1",
+                                       "name": "contact1"})
+        self.contacts.append(contact)
+
+        # No 'name' is given here, so the name will be made from givenname.
+        contact = self._randomContact({"expectedname": "contact2",
+                                       "givenName": "contact2"})
+        self.contacts.append(contact)
+
+        contact = self._randomContact({"expectedname": "contact3",
+                                       "name": "contact3",
+                                       "displayName": "contact3displayname",
+                                       "givenName": "not_contact3",
+                                       "initials": "I",
+                                       "sn": "not_contact3",
+                                       "mobile": "12345"})
+        self.contacts.append(contact)
+
+        # No 'name' is given here, so the name will be made from the the
+        # sn, initials and givenName attributes.
+        contact = self._randomContact({"expectedname": "James T. Kirk",
+                                       "sn": "Kirk",
+                                       "initials": "T",
+                                       "givenName": "James"})
+        self.contacts.append(contact)
+
+        # setup the 4 contacts and ensure they are correct
+        for contact in self.contacts:
+            (result, out, err) = self._create_contact(contact)
+
+            self.assertCmdSuccess(result, out, err)
+            self.assertNotIn(
+                "ERROR", err, "There shouldn't be any error message")
+            self.assertIn("Contact '%s' created successfully" %
+                          contact["expectedname"], out)
+
+            found = self._find_contact(contact["expectedname"])
+
+            self.assertIsNotNone(found)
+
+            contactname = contact["expectedname"]
+            self.assertEquals("%s" % found.get("name"), contactname)
+            self.assertEquals("%s" % found.get("description"),
+                              contact["description"])
+
+    def tearDown(self):
+        super(ContactCmdTestCase, self).tearDown()
+        # clean up all the left over contacts, just in case
+        for contact in self.contacts:
+            if self._find_contact(contact["expectedname"]):
+                (result, out, err) = self.runsubcmd(
+                    "contact", "delete", "%s" % contact["expectedname"])
+                self.assertCmdSuccess(result, out, err,
+                                      "Failed to delete contact '%s'" %
+                                      contact["expectedname"])
+
+    def test_newcontact(self):
+        """This tests the "contact create" and "contact delete" commands"""
+        # try to create all the contacts again, this should fail
+        for contact in self.contacts:
+            (result, out, err) = self._create_contact(contact)
+            self.assertCmdFail(result, "Succeeded to create existing contact")
+            self.assertIn("already exists", err)
+
+        # try to delete all the contacts we just created
+        for contact in self.contacts:
+            (result, out, err) = self.runsubcmd("contact", "delete", "%s" %
+                                                contact["expectedname"])
+            self.assertCmdSuccess(result, out, err,
+                                  "Failed to delete contact '%s'" %
+                                  contact["expectedname"])
+            found = self._find_contact(contact["expectedname"])
+            self.assertIsNone(found,
+                              "Deleted contact '%s' still exists" %
+                              contact["expectedname"])
+
+        # test creating contacts in an specified OU
+        parentou = self._randomOU({"name": "testOU"})
+        (result, out, err) = self._create_ou(parentou)
+        self.assertCmdSuccess(result, out, err)
+
+        for contact in self.contacts:
+            (result, out, err) = self._create_contact(contact, ou="OU=testOU")
+
+            self.assertCmdSuccess(result, out, err)
+            self.assertEquals(err, "", "There shouldn't be any error message")
+            self.assertIn("Contact '%s' created successfully" %
+                          contact["expectedname"], out)
+
+            found = self._find_contact(contact["expectedname"])
+
+            contactname = contact["expectedname"]
+            self.assertEquals("%s" % found.get("name"), contactname)
+            self.assertEquals("%s" % found.get("description"),
+                              contact["description"])
+
+        # try to delete all the contacts we just created, by DN
+        for contact in self.contacts:
+            expecteddn = ldb.Dn(self.samdb,
+                                "CN=%s,OU=%s,%s" %
+                                (contact["expectedname"],
+                                 parentou["name"],
+                                 self.samdb.domain_dn()))
+            (result, out, err) = self.runsubcmd("contact", "delete", "%s" %
+                                                expecteddn)
+            self.assertCmdSuccess(result, out, err,
+                                  "Failed to delete contact '%s'" %
+                                  contact["expectedname"])
+            found = self._find_contact(contact["expectedname"])
+            self.assertIsNone(found,
+                              "Deleted contact '%s' still exists" %
+                              contact["expectedname"])
+
+        (result, out, err) = self.runsubcmd("ou", "delete",
+                                            "OU=%s" % parentou["name"])
+        self.assertCmdSuccess(result, out, err,
+                              "Failed to delete ou '%s'" % parentou["name"])
+
+        # creating contacts, again for further tests
+        for contact in self.contacts:
+            (result, out, err) = self._create_contact(contact)
+
+            self.assertCmdSuccess(result, out, err)
+            self.assertEquals(err, "", "There shouldn't be any error message")
+            self.assertIn("Contact '%s' created successfully" %
+                          contact["expectedname"], out)
+
+            found = self._find_contact(contact["expectedname"])
+
+            contactname = contact["expectedname"]
+            self.assertEquals("%s" % found.get("name"), contactname)
+            self.assertEquals("%s" % found.get("description"),
+                              contact["description"])
+
+    def test_list(self):
+        (result, out, err) = self.runsubcmd("contact", "list")
+        self.assertCmdSuccess(result, out, err, "Error running list")
+
+        search_filter = "(objectClass=contact)"
+        contactlist = self.samdb.search(base=self.samdb.domain_dn(),
+                                         scope=ldb.SCOPE_SUBTREE,
+                                         expression=search_filter,
+                                         attrs=["name", "displayName"])
+
+        self.assertTrue(len(contactlist) > 0, "no contacts found in samdb")
+
+        for contactobj in contactlist:
+            name = contactobj.get("displayName", idx=0)
+            if name == None:
+                name = contactobj.get("name", idx=0)
+            self.assertMatch(out, str(name),
+                             "contact '%s' not found" % name)
+
+    def test_list_full_dn(self):
+        (result, out, err) = self.runsubcmd("contact", "list", "--full-dn")
+        self.assertCmdSuccess(result, out, err, "Error running list")
+
+        search_filter = "(objectClass=contact)"
+        contactlist = self.samdb.search(base=self.samdb.domain_dn(),
+                                         scope=ldb.SCOPE_SUBTREE,
+                                         expression=search_filter,
+                                         attrs=["dn"])
+
+        self.assertTrue(len(contactlist) > 0, "no contacts found in samdb")
+
+        for contactobj in contactlist:
+            self.assertMatch(out, str(contactobj.dn),
+                             "contact '%s' not found" % str(contactobj.dn))
+
+    def test_move(self):
+        parentou = self._randomOU({"name": "parentOU"})
+        (result, out, err) = self._create_ou(parentou)
+        self.assertCmdSuccess(result, out, err)
+
+        for contact in self.contacts:
+            olddn = self._find_contact(contact["expectedname"]).get("dn")
+
+            (result, out, err) = self.runsubcmd("contact", "move",
+                                                "%s" % contact["expectedname"],
+                                                "OU=%s" % parentou["name"])
+            self.assertCmdSuccess(result, out, err,
+                                  "Failed to move contact '%s'" %
+                                  contact["expectedname"])
+            self.assertEquals(err, "", "There shouldn't be any error message")
+            self.assertIn('Moved contact "%s"' % contact["expectedname"], out)
+
+            found = self._find_contact(contact["expectedname"])
+            self.assertNotEquals(found.get("dn"), olddn,
+                                 ("Moved contact '%s' still exists with the "
+                                  "same dn" % contact["expectedname"]))
+            contactname = contact["expectedname"]
+            newexpecteddn = ldb.Dn(self.samdb,
+                                   "CN=%s,OU=%s,%s" %
+                                   (contactname,
+                                    parentou["name"],
+                                    self.samdb.domain_dn()))
+            self.assertEquals(found.get("dn"), newexpecteddn,
+                              "Moved contact '%s' does not exist" %
+                              contact["expectedname"])
+
+            (result, out, err) = self.runsubcmd("contact", "move",
+                                                "%s" % contact["expectedname"],
+                                                "%s" % olddn.parent())
+            self.assertCmdSuccess(result, out, err,
+                                  "Failed to move contact '%s'" %
+                                  contact["expectedname"])
+
+        (result, out, err) = self.runsubcmd("ou", "delete",
+                                            "OU=%s" % parentou["name"])
+        self.assertCmdSuccess(result, out, err,
+                              "Failed to delete ou '%s'" % parentou["name"])
+
+    def _randomContact(self, base={}):
+        """Create a contact with random attribute values, you can specify base
+        attributes"""
+
+        # No name attributes are given here, because the object name will
+        # be made from the sn, givenName and initials attributes, if no name
+        # is given.
+        contact = {
+            "description": self.randomName(count=100),
+        }
+        contact.update(base)
+        return contact
+
+    def _randomOU(self, base={}):
+        """Create an ou with random attribute values, you can specify base
+        attributes."""
+
+        ou = {
+            "name": self.randomName(),
+            "description": self.randomName(count=100),
+        }
+        ou.update(base)
+        return ou
+
+    def _create_contact(self, contact, ou=None):
+        args = ""
+
+        if "name" in contact:
+            args += '{0}'.format(contact['name'])
+
+        args += ' {0}'.format(self.creds)
+
+        if ou is not None:
+            args += ' --ou={0}'.format(ou)
+
+        if "description" in contact:
+            args += ' --description={0}'.format(contact["description"])
+        if "sn" in contact:
+            args += ' --surname={0}'.format(contact["sn"])
+        if "initials" in contact:
+            args += ' --initials={0}'.format(contact["initials"])
+        if "givenName" in contact:
+            args += ' --given-name={0}'.format(contact["givenName"])
+        if "displayName" in contact:
+            args += ' --display-name={0}'.format(contact["displayName"])
+        if "mobile" in contact:
+            args += ' --mobile-number={0}'.format(contact["mobile"])
+
+        args = args.split()
+
+        return self.runsubcmd('contact', 'create', *args)
+
+    def _create_ou(self, ou):
+        return self.runsubcmd("ou",
+                              "create",
+                              "OU=%s" % ou["name"],
+                              "--description=%s" % ou["description"])
+
+    def _find_contact(self, name):
+        contactname = name
+        search_filter = ("(&(objectClass=contact)(name=%s))" %
+                         ldb.binary_encode(contactname))
+        contactlist = self.samdb.search(base=self.samdb.domain_dn(),
+                                        scope=ldb.SCOPE_SUBTREE,
+                                        expression=search_filter,
+                                        attrs=[])
+        if contactlist:
+            return contactlist[0]
+        else:
+            return None
diff --git a/python/samba/tests/samba_tool/contact_edit.sh b/python/samba/tests/samba_tool/contact_edit.sh
new file mode 100755
index 00000000000..ca38900062a
--- /dev/null
+++ b/python/samba/tests/samba_tool/contact_edit.sh
@@ -0,0 +1,164 @@
+#!/bin/sh
+#
+# Test for 'samba-tool contact edit'
+
+if [ $# -lt 3 ]; then
+cat <<EOF
+Usage: contact_edit.sh SERVER USERNAME PASSWORD
+EOF
+exit 1;
+fi
+
+SERVER="$1"
+USERNAME="$2"
+PASSWORD="$3"
+
+STpath=$(pwd)
+. $STpath/testprogs/blackbox/subunit.sh
+
+display_name="Björn"
+display_name_b64="QmrDtnJu"
+display_name_new="Renamed Bjoern"
+# attribute value including control character
+# echo -e "test \a string" | base64
+display_name_con_b64="dGVzdCAHIHN0cmluZwo="
+
+tmpeditor=$(mktemp --suffix .sh -p $STpath/bin samba-tool-editor-XXXXXXXX)
+chmod +x $tmpeditor
+
+create_test_contact() {
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool \
+		contact create testcontact1 \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+# Test edit contact - add base64 attributes
+add_attribute_base64() {
+	# create editor.sh
+	cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+contact_ldif="\$1"
+
+grep -v '^$' \$contact_ldif > \${contact_ldif}.tmp
+echo "displayName:: $display_name_b64" >> \${contact_ldif}.tmp
+
+mv \${contact_ldif}.tmp \$contact_ldif
+EOF
+
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool contact edit \
+		testcontact1 --editor=$tmpeditor \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_attribute_base64() {
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool contact show \
+		testcontact1 --attributes=displayName \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+delete_attribute() {
+	# create editor.sh
+	cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+contact_ldif="\$1"
+
+grep -v '^displayName' \$contact_ldif >> \${contact_ldif}.tmp
+mv \${contact_ldif}.tmp \$contact_ldif
+EOF
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool contact edit \
+		testcontact1 --editor=$tmpeditor \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+# Test edit contact - add base64 attribute value including control character
+add_attribute_base64_control() {
+	# create editor.sh
+	cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+contact_ldif="\$1"
+
+grep -v '^$' \$contact_ldif > \${contact_ldif}.tmp
+echo "displayName:: $display_name_con_b64" >> \${contact_ldif}.tmp
+
+mv \${contact_ldif}.tmp \$contact_ldif
+EOF
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool contact edit \
+		testcontact1 --editor=$tmpeditor \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_attribute_base64_control() {
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool contact show \
+		testcontact1 --attributes=displayName \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+
+# Test edit contact - change base64 attribute value including control character
+change_attribute_base64_control() {
+	# create editor.sh
+	cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+contact_ldif="\$1"
+
+sed -i -e 's/displayName:: $display_name_con_b64/displayName: $display_name/' \
+	\$contact_ldif
+EOF
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool contact edit \
+		testcontact1 --editor=$tmpeditor \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_attribute_base64_control() {
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool contact show \
+		testcontact1 --attributes=displayName \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+# Test edit contact - change attributes with LDB_FLAG_FORCE_NO_BASE64_LDIF
+change_attribute_force_no_base64() {
+	# create editor.sh
+	# Expects that the original attribute is available as clear text,
+	# because the LDB_FLAG_FORCE_NO_BASE64_LDIF should be used here.
+	cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+contact_ldif="\$1"
+
+sed -i -e 's/displayName: $display_name/displayName: $display_name_new/' \
+	\$contact_ldif
+EOF
+
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool contact edit \
+		testcontact1 --editor=$tmpeditor \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_changed_attribute_force_no_base64() {
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool contact show \
+		 testcontact1 --attributes=displayName \
+		 -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+delete_contact() {
+	$PYTHON ${STpath}/source4/scripting/bin/samba-tool \
+		contact delete testcontact1 \
+		-H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+failed=0
+
+testit "create_test_contact" create_test_contact || failed=`expr $failed + 1`
+testit "add_attribute_base64" add_attribute_base64 || failed=`expr $failed + 1`
+testit_grep "get_attribute_base64" "^displayName:: $display_name_b64" get_attribute_base64 || failed=`expr $failed + 1`
+testit "delete_attribute" delete_attribute || failed=`expr $failed + 1`
+testit "add_attribute_base64_control" add_attribute_base64_control || failed=`expr $failed + 1`
+testit_grep "get_attribute_base64_control" "^displayName:: $display_name_con_b64" get_attribute_base64_control || failed=`expr $failed + 1`
+testit "change_attribute_base64_control" change_attribute_base64_control || failed=`expr $failed + 1`
+testit_grep "get_attribute_base64_control" "^displayName:: $display_name_b64" get_attribute_base64_control || failed=`expr $failed + 1`
+testit "change_attribute_force_no_base64" change_attribute_force_no_base64 || failed=`expr $failed + 1`
+testit_grep "get_changed_attribute_force_no_base64" "^displayName: $display_name_new" get_changed_attribute_force_no_base64 || failed=`expr $failed + 1`
+testit "delete_contact" delete_contact || failed=`expr $failed + 1`
+
+rm -f $tmpeditor
+
+exit $failed
diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py
index cc2410537b4..eb0ab38de66 100755
--- a/source4/selftest/tests.py
+++ b/source4/selftest/tests.py
@@ -654,11 +654,12 @@ planpythontestsuite("none", "samba.tests.samba_tool.visualize", py3_compatible=T
 for env in all_fl_envs:
     planpythontestsuite(env + ":local", "samba.tests.samba_tool.fsmo", py3_compatible=True)
 
-# test samba-tool user, group and computer edit command
+# test samba-tool user, group, contact and computer edit command
 for env in all_fl_envs:
     env += ":local"
     plantestsuite("samba.tests.samba_tool.user_edit", env, [os.path.join(srcdir(), "python/samba/tests/samba_tool/user_edit.sh"), '$SERVER', '$USERNAME', '$PASSWORD'])
     plantestsuite("samba.tests.samba_tool.group_edit", env, [os.path.join(srcdir(), "python/samba/tests/samba_tool/group_edit.sh"), '$SERVER', '$USERNAME', '$PASSWORD'])
+    plantestsuite("samba.tests.samba_tool.contact_edit", env, [os.path.join(srcdir(), "python/samba/tests/samba_tool/contact_edit.sh"), '$SERVER', '$USERNAME', '$PASSWORD'])
     plantestsuite("samba.tests.samba_tool.computer_edit", env, [os.path.join(srcdir(), "python/samba/tests/samba_tool/computer_edit.sh"), '$SERVER', '$USERNAME', '$PASSWORD'])
 
 # We run this test against both AD DC implementations because it is
@@ -679,6 +680,7 @@ planpythontestsuite("chgdcpass:local", "samba.tests.samba_tool.user_check_passwo
 planpythontestsuite("ad_dc_default:local", "samba.tests.samba_tool.group", py3_compatible=True)
 planpythontestsuite("ad_dc_default:local", "samba.tests.samba_tool.ou", py3_compatible=True)
 planpythontestsuite("ad_dc_default:local", "samba.tests.samba_tool.computer", py3_compatible=True)
+planpythontestsuite("ad_dc_default:local", "samba.tests.samba_tool.contact", py3_compatible=True)
 planpythontestsuite("ad_dc_default:local", "samba.tests.samba_tool.forest", py3_compatible=True)
 planpythontestsuite("ad_dc_default:local", "samba.tests.samba_tool.schema", py3_compatible=True)
 planpythontestsuite("ad_dc:local", "samba.tests.samba_tool.ntacl", py3_compatible=True)
-- 
2.19.2



More information about the samba-technical mailing list