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

Björn Baumbach bb at sernet.de
Thu Mar 28 21:02:54 UTC 2019


On 3/22/19 6:19 PM, Björn Baumbach via samba-technical wrote:
> I did some work to improve the "edit" sub-command of the samba-tool and
> need a review:
> 
> 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.

Hi!

Please find attached an updated version of the patch set.

I've noticed that the documentation for the new commands was missing and
added that to the patch set. Furthermore I've fixed some typos, removed
the option to use the contact objects by displayName and increased the
ldb library version.

There is also a pipeline:
https://gitlab.com/samba-team/devel/samba/pipelines/54131082

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
-------------- next part --------------
From d5c96cb07d95aaa7e7ea35da49724e734ff55ace 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/21] 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 <bb 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 605202d928645d439595eaecd80017bc3814b05a 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/21] 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 <bb 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 5a3f69f232d..4cd8dc49b69 100755
--- a/source4/selftest/tests.py
+++ b/source4/selftest/tests.py
@@ -651,7 +651,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 0d18d54dd121c701fc279c0d75467e2b8deba3a2 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/21] 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 <bb 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 82d8e2de903c6f8f24b8b3431dafa28ff05012ef 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/21] 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 <bb 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 eedc41b9798c887901b463ee19e7944fe7559397 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/21] 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 <bb 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 eec80181bad672baee7472f3e5ee00041236c2e4 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/21] 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 <bb 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 a040b5d7486c6bc1522f966be861336283d86808 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B6rn=20Baumbach?= <bb at sernet.de>
Date: Fri, 15 Mar 2019 15:27:36 +0100
Subject: [PATCH 07/21] ldb: version 1.6.4
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Enable make test even without lmdb
  BUG: https://bugzilla.samba.org/show_bug.cgi?id=13630
* Add LDB_FLAG_FORCE_NO_BASE64_LDIF

Signed-off-by: Björn Baumbach <bb at samba.org>
---
 lib/ldb/ABI/ldb-1.6.4.sigs        | 280 ++++++++++++++++++++++++++++++
 lib/ldb/ABI/pyldb-util-1.6.4.sigs |   2 +
 lib/ldb/wscript                   |   2 +-
 3 files changed, 283 insertions(+), 1 deletion(-)
 create mode 100644 lib/ldb/ABI/ldb-1.6.4.sigs
 create mode 100644 lib/ldb/ABI/pyldb-util-1.6.4.sigs

diff --git a/lib/ldb/ABI/ldb-1.6.4.sigs b/lib/ldb/ABI/ldb-1.6.4.sigs
new file mode 100644
index 00000000000..0c1234f1c97
--- /dev/null
+++ b/lib/ldb/ABI/ldb-1.6.4.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.4.sigs b/lib/ldb/ABI/pyldb-util-1.6.4.sigs
new file mode 100644
index 00000000000..74d6719d2bc
--- /dev/null
+++ b/lib/ldb/ABI/pyldb-util-1.6.4.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 7891693a0fd..b8746e421e7 100644
--- a/lib/ldb/wscript
+++ b/lib/ldb/wscript
@@ -1,7 +1,7 @@
 #!/usr/bin/env python
 
 APPNAME = 'ldb'
-VERSION = '1.6.3'
+VERSION = '1.6.4'
 
 import sys, os
 
-- 
2.19.2


From c1256cc6c0372c15eda3fbe3d7efa3775ba57b6f 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/21] 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 <bb 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 901a37982c5b9b10427c98f4956e5e6df4b1a1ad 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/21] 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 <bb 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 6893b6b8a01d51814d020dac7e21263bf1488b20 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/21] 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 <bb 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 7abd2f859b0827ec2f374aafa17a17ce3e74db16 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/21] 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 <bb 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 aab52e9596c3490262e3dbd5c393e1cea002fd0a 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/21] 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 <bb 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 daaa30abd306785038c9f64af89a1573179fa08f 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/21] 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 <bb 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 f57628753d95895021162efbe39f3ab5899fd1b5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B6rn=20Baumbach?= <bb at sernet.de>
Date: Thu, 28 Mar 2019 20:34:20 +0100
Subject: [PATCH 14/21] doc: add samba-tool computer command to samba-tool man
 page
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Björn Baumbach <bb at samba.org>
---
 docs-xml/manpages/samba-tool.8.xml | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/docs-xml/manpages/samba-tool.8.xml b/docs-xml/manpages/samba-tool.8.xml
index 01f5313abf8..16daf6c6460 100644
--- a/docs-xml/manpages/samba-tool.8.xml
+++ b/docs-xml/manpages/samba-tool.8.xml
@@ -164,6 +164,23 @@
 	sAMAccountName, with or without the trailing dollar sign.</para>
 </refsect3>
 
+<refsect3>
+	<title>computer edit <replaceable>computername</replaceable></title>
+	<para>Edit a computer AD object.</para>
+	<para>The computer name specified on the command is the
+	sAMAccountName, with or without the trailing dollar sign.</para>
+
+	<variablelist>
+	<varlistentry>
+	<term>--editor=EDITOR</term>
+	<listitem><para>
+	Specifies the editor to use instead of the system default, or 'vi' if no
+	system default is set.
+	</para></listitem>
+	</varlistentry>
+	</variablelist>
+</refsect3>
+
 <refsect3>
 	<title>computer list</title>
 	<para>List all computers.</para>
-- 
2.19.2


From e079ad61e3a5a819d383e39fdaf866d2eef39fe2 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 15/21] 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 <bb 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 4cd8dc49b69..ccd81a0abc8 100755
--- a/source4/selftest/tests.py
+++ b/source4/selftest/tests.py
@@ -648,10 +648,11 @@ planpythontestsuite("none", "samba.tests.samba_tool.visualize")
 for env in all_fl_envs:
     planpythontestsuite(env + ":local", "samba.tests.samba_tool.fsmo")
 
-# 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 67b1d99af718d392bdc184f7089d68978225a184 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 16/21] 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 <bb 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 1d0b1da2e16a9867385731721a27902be17a928f 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 17/21] 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 <bb 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 ccd81a0abc8..acd1788e01c 100755
--- a/source4/selftest/tests.py
+++ b/source4/selftest/tests.py
@@ -648,10 +648,11 @@ planpythontestsuite("none", "samba.tests.samba_tool.visualize")
 for env in all_fl_envs:
     planpythontestsuite(env + ":local", "samba.tests.samba_tool.fsmo")
 
-# 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 a51f613d0b42c57122665805127a033e56463358 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B6rn=20Baumbach?= <bb at sernet.de>
Date: Thu, 28 Mar 2019 20:31:48 +0100
Subject: [PATCH 18/21] doc: add samba-tool group command to samba-tool man
 page
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Björn Baumbach <bb at samba.org>
---
 docs-xml/manpages/samba-tool.8.xml | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/docs-xml/manpages/samba-tool.8.xml b/docs-xml/manpages/samba-tool.8.xml
index 16daf6c6460..be0dece2c08 100644
--- a/docs-xml/manpages/samba-tool.8.xml
+++ b/docs-xml/manpages/samba-tool.8.xml
@@ -630,6 +630,21 @@
 	<para>Delete an AD group.</para>
 </refsect3>
 
+<refsect3>
+	<title>group edit <replaceable>groupname</replaceable></title>
+	<para>Edit a group AD object.</para>
+
+	<variablelist>
+	<varlistentry>
+	<term>--editor=EDITOR</term>
+	<listitem><para>
+	Specifies the editor to use instead of the system default, or 'vi' if no
+	system default is set.
+	</para></listitem>
+	</varlistentry>
+	</variablelist>
+</refsect3>
+
 <refsect3>
 	<title>group list</title>
 	<para>List all groups.</para>
-- 
2.19.2


From 9672de9ded95731f47ccb61a6f9b779288ae7e8a 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 19/21] 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 <bb at samba.org>
---
 python/samba/netcmd/contact.py | 676 +++++++++++++++++++++++++++++++++
 python/samba/netcmd/main.py    |   1 +
 python/samba/samdb.py          | 108 ++++++
 3 files changed, 785 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..506e644c3f8
--- /dev/null
+++ b/python/samba/netcmd/contact.py
@@ -0,0 +1,676 @@
+# samba-tool contact management
+#
+# Copyright Bjoern 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 arguments.
+    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 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))" %
+                  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.
+    """
+
+    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"])
+        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_name = msg.get("name", idx=0)
+
+            self.outf.write("%s\n" % contact_name)
+
+
+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 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))" %
+                   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 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))" %
+                  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 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))" %
+                  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 cb8f5d79341c81605dbf0d3a364ea4507072bf74 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 20/21] 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 <bb at samba.org>
---
 python/samba/tests/samba_tool/contact.py      | 319 ++++++++++++++++++
 python/samba/tests/samba_tool/contact_edit.sh | 164 +++++++++
 source4/selftest/tests.py                     |   3 +-
 3 files changed, 485 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..626277ce8f1
--- /dev/null
+++ b/python/samba/tests/samba_tool/contact.py
@@ -0,0 +1,319 @@
+# Unix SMB/CIFS implementation.
+#
+# Tests for samba-tool contact management commands
+#
+# Copyright (C) Bjoern 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"])
+
+        self.assertTrue(len(contactlist) > 0, "no contacts found in samdb")
+
+        for contactobj in contactlist:
+            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 acd1788e01c..2d4578e7bc8 100755
--- a/source4/selftest/tests.py
+++ b/source4/selftest/tests.py
@@ -648,11 +648,12 @@ planpythontestsuite("none", "samba.tests.samba_tool.visualize")
 for env in all_fl_envs:
     planpythontestsuite(env + ":local", "samba.tests.samba_tool.fsmo")
 
-# 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
-- 
2.19.2


From a37be119c724bb75e09e19ea5228f2ee91011aae Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B6rn=20Baumbach?= <bb at sernet.de>
Date: Thu, 28 Mar 2019 20:39:41 +0100
Subject: [PATCH 21/21] doc: add documentation for "samba-tool" contact
 management
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Björn Baumbach <bb at samba.org>
---
 docs-xml/manpages/samba-tool.8.xml | 186 +++++++++++++++++++++++++++++
 1 file changed, 186 insertions(+)

diff --git a/docs-xml/manpages/samba-tool.8.xml b/docs-xml/manpages/samba-tool.8.xml
index be0dece2c08..ca2bc1be763 100644
--- a/docs-xml/manpages/samba-tool.8.xml
+++ b/docs-xml/manpages/samba-tool.8.xml
@@ -212,6 +212,192 @@
 	</variablelist>
 </refsect3>
 
+<refsect2>
+	<title>contact</title>
+	<para>Manage contacts.</para>
+</refsect2>
+
+<refsect3>
+	<title>contact create [<replaceable>contactname</replaceable>] [options]</title>
+	<para>Create a new contact in the Active Directory Domain.</para>
+	<para>The name of the new contact can be specified by the first
+	argument 'contactname' or the --given-name, --initial and --surname
+	arguments. 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.</para>
+
+	<variablelist>
+	<varlistentry>
+	<term>--ou=OU</term>
+	<listitem><para>
+	DN of alternative location (with or without domainDN counterpart) in
+	which the new contact will be created.
+	E.g. 'OU=OUname'.
+	Default is the domain base.
+	</para></listitem>
+	</varlistentry>
+
+	<varlistentry>
+	<term>--description=DESCRIPTION</term>
+	<listitem><para>
+	The new contacts's description.
+	</para></listitem>
+	</varlistentry>
+
+	<varlistentry>
+	<term>--surname=SURNAME</term>
+	<listitem><para>
+	Contact's surname.
+	</para></listitem>
+	</varlistentry>
+
+	<varlistentry>
+	<term>--given-name=GIVEN_NAME</term>
+	<listitem><para>
+	Contact's given name.
+	</para></listitem>
+	</varlistentry>
+
+	<varlistentry>
+	<term>--initials=INITIALS</term>
+	<listitem><para>
+	Contact's initials.
+	</para></listitem>
+	</varlistentry>
+
+	<varlistentry>
+	<term>--display-name=DISPLAY_NAME</term>
+	<listitem><para>
+	Contact's display name.
+	</para></listitem>
+	</varlistentry>
+
+	<varlistentry>
+	<term>--job-title=JOB_TITLE</term>
+	<listitem><para>
+	Contact's job title.
+	</para></listitem>
+	</varlistentry>
+
+	<varlistentry>
+	<term>--department=DEPARTMENT</term>
+	<listitem><para>
+	Contact's department.
+	</para></listitem>
+	</varlistentry>
+
+	<varlistentry>
+	<term>--company=COMPANY</term>
+	<listitem><para>
+	Contact's company.
+	</para></listitem>
+	</varlistentry>
+
+	<varlistentry>
+	<term>--mail-address=MAIL_ADDRESS</term>
+	<listitem><para>
+	Contact's email address.
+	</para></listitem>
+	</varlistentry>
+
+	<varlistentry>
+	<term>--internet-address=INTERNET_ADDRESS</term>
+	<listitem><para>
+	Contact's home page.
+	</para></listitem>
+	</varlistentry>
+
+	<varlistentry>
+	<term>--telephone-number=TELEPHONE_NUMBER</term>
+	<listitem><para>
+	Contact's phone number.
+	</para></listitem>
+	</varlistentry>
+
+	<varlistentry>
+	<term>--mobile-number=MOBILE_NUMBER</term>
+	<listitem><para>
+	Contact's mobile phone number.
+	</para></listitem>
+	</varlistentry>
+
+	<varlistentry>
+	<term>--physical-delivery-office=PHYSICAL_DELIVERY_OFFICE</term>
+	<listitem><para>
+	Contact's office location.
+	</para></listitem>
+	</varlistentry>
+
+	</variablelist>
+</refsect3>
+
+<refsect3>
+	<title>contact delete <replaceable>contactname</replaceable> [options]</title>
+	<para>Delete an existing contact.</para>
+	<para>The contactname specified on the command is the 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.</para>
+</refsect3>
+
+<refsect3>
+	<title>contact edit <replaceable>contactname</replaceable></title>
+	<para>Modify a contact AD object.</para>
+	<para>The contactname specified on the command is the 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.</para>
+
+	<variablelist>
+	<varlistentry>
+	<term>--editor=EDITOR</term>
+	<listitem><para>
+	Specifies the editor to use instead of the system default, or 'vi' if no
+	system default is set.
+	</para></listitem>
+	</varlistentry>
+	</variablelist>
+</refsect3>
+
+<refsect3>
+	<title>contact list [options]</title>
+	<para>List all contacts.</para>
+
+	<variablelist>
+	<varlistentry>
+	<term>--full-dn</term>
+	<listitem><para>
+	Display contact's full DN instead of the name.
+	</para></listitem>
+	</varlistentry>
+	</variablelist>
+</refsect3>
+
+<refsect3>
+	<title>contact move <replaceable>contactname</replaceable> <replaceable>new_parent_dn</replaceable> [options]</title>
+	<para>This command moves a contact into the specified organizational
+	unit or container.</para>
+	<para>The contactname specified on the command is the 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.</para>
+</refsect3>
+
+<refsect3>
+	<title>contact show <replaceable>contactname</replaceable> [options]</title>
+	<para>Display a contact AD object.</para>
+	<para>The contactname specified on the command is the 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.</para>
+
+	<variablelist>
+	<varlistentry>
+	<term>--attributes=CONTACT_ATTRS</term>
+	<listitem><para>
+	Comma separated list of attributes, which will be printed.
+	</para></listitem>
+	</varlistentry>
+	</variablelist>
+</refsect3>
+
 <refsect2>
 	<title>dbcheck</title>
 	<para>Check the local AD database for errors.</para>
-- 
2.19.2



More information about the samba-technical mailing list