[PATCH] samba-tools: add computer subcommand

joeg at catalyst.net.nz joeg at catalyst.net.nz
Tue Feb 6 18:49:36 UTC 2018


Oops, the patch!

BTW:

this patch is based on my previous dns cleanup command and Garming's
improvement patch.

So please merge this one after those 2 are merged.

Thanks!


On 07/02/18 07:46, joeg at catalyst.net.nz wrote:
>
> Hi Team,
>
> I added a new `computer` subcommand for samba-tools, which has the
> same set of commands as the `user` command, but work for a computer.
> Most of the code are copied from user command and then adjust for
> computer.
>
> Review appreciated!
>
> Thanks, Joe.
>
> -- 
> Joe Guo
> joeg at catalyst.net.nz
> Catalyst IT

-- 
Joe Guo
joeg at catalyst.net.nz
Catalyst IT

-------------- next part --------------
From 7121f28f42389ee4d4dc41136d2662a55feb8210 Mon Sep 17 00:00:00 2001
From: Joe Guo <joeg at catalyst.net.nz>
Date: Thu, 1 Feb 2018 10:59:58 +1300
Subject: [PATCH 01/12] samba-tools/computer: register computer subcommand

Signed-off-by: Joe Guo  <joeg at catalyst.net.nz>
---
 python/samba/netcmd/main.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/python/samba/netcmd/main.py b/python/samba/netcmd/main.py
index 7f94f89..1d565e2 100644
--- a/python/samba/netcmd/main.py
+++ b/python/samba/netcmd/main.py
@@ -75,5 +75,6 @@ class cmd_sambatool(SuperCommand):
     subcommands["testparm"] = None
     subcommands["time"] = None
     subcommands["user"] = None
+    subcommands["computer"] = None
     subcommands["processes"] = None
     subcommands["visualize"] = None
-- 
2.7.4


From 6c2ec64e260704a54d2dd528ea62f29a07c0d766 Mon Sep 17 00:00:00 2001
From: Joe Guo <joeg at catalyst.net.nz>
Date: Thu, 1 Feb 2018 11:01:01 +1300
Subject: [PATCH 02/12] samdb: add newcomputer method

samba-tools computer command will use it.

Signed-off-by: Joe Guo  <joeg at catalyst.net.nz>
---
 python/samba/samdb.py | 43 +++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 43 insertions(+)

diff --git a/python/samba/samdb.py b/python/samba/samdb.py
index 4645629..e2cd3af 100644
--- a/python/samba/samdb.py
+++ b/python/samba/samdb.py
@@ -297,6 +297,49 @@ member: %s
         else:
             self.transaction_commit()
 
+
+    def newcomputer(self, computername, password,
+                    force_password_change_at_next_login_req=False,
+                    computerou=None, setpassword=True,
+                    serviceprincipalname=None):
+        """Adds a new computer with additional parameters
+
+        :param computername: Name of the new computer(without ending $)
+        :param password: Password for the new computer
+        :param force_password_change_at_next_login_req: Force password change at next login
+        :param computerou: Object container (without domainDN postfix) for new computer
+        :param serviceprincipalname: Service Principal Name list for the new computer
+        """
+        computerou = computerou or "CN=Computers"
+        # for computer, sAMAccountName must end with a $
+        sAMAccountName = ldb.binary_encode(computername) + '$'
+
+        ldbmessage = {
+            'dn': "CN=%s,%s,%s" % (computername, computerou, self.domain_dn()),
+            'sAMAccountName': sAMAccountName,
+            'userAccountControl': str(dsdb.UF_WORKSTATION_TRUST_ACCOUNT),
+            'dNSHostName': '{}.{}'.format(computername, self.domain_dns_name()),
+            'servicePrincipalName': serviceprincipalname,
+            'objectClass': 'computer',
+        }
+
+        self.transaction_start()
+        try:
+            self.add(ldbmessage)
+
+            # Sets the password for it
+            if setpassword:
+                self.setpassword(
+                    "(sAMAccountName=%s)" % sAMAccountName, # alreay ends with $
+                    password,
+                    force_password_change_at_next_login_req)
+
+        except:
+            self.transaction_cancel()
+            raise
+        else:
+            self.transaction_commit()
+
     def newuser(self, username, password,
             force_password_change_at_next_login_req=False,
             useusernameascn=False, userou=None, surname=None, givenname=None,
-- 
2.7.4


From fb1f723623b6049d5d43bbe6d0e5259af64798e8 Mon Sep 17 00:00:00 2001
From: Joe Guo <joeg at catalyst.net.nz>
Date: Fri, 26 Jan 2018 14:36:48 +1300
Subject: [PATCH 03/12] samba-tools/computer: copy user.py to computer.py

Just copy in this commits, changes are made step by step in following
commits.

Signed-off-by: Joe Guo  <joeg at catalyst.net.nz>
---
 python/samba/netcmd/computer.py | 2437 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 2437 insertions(+)
 create mode 100644 python/samba/netcmd/computer.py

diff --git a/python/samba/netcmd/computer.py b/python/samba/netcmd/computer.py
new file mode 100644
index 0000000..3b744a3
--- /dev/null
+++ b/python/samba/netcmd/computer.py
@@ -0,0 +1,2437 @@
+# user management
+#
+# Copyright Jelmer Vernooij 2010 <jelmer at samba.org>
+# Copyright Theresa Halloran 2011 <theresahalloran at gmail.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+import samba.getopt as options
+import ldb
+import pwd
+import os
+import re
+import tempfile
+import difflib
+import sys
+import fcntl
+import signal
+import errno
+import time
+import base64
+import binascii
+from subprocess import Popen, PIPE, STDOUT, check_call, CalledProcessError
+from getpass import getpass
+from samba.auth import system_session
+from samba.samdb import SamDB
+from samba.dcerpc import misc
+from samba.dcerpc import security
+from samba.dcerpc import drsblobs
+from samba.ndr import ndr_unpack, ndr_pack, ndr_print
+from samba import (
+    credentials,
+    dsdb,
+    gensec,
+    generate_random_password,
+    Ldb,
+    )
+from samba.net import Net
+
+from samba.netcmd import (
+    Command,
+    CommandError,
+    SuperCommand,
+    Option,
+    )
+
+
+try:
+    import io
+    import gpgme
+    gpgme_support = True
+    decrypt_samba_gpg_help = "Decrypt the SambaGPG password as cleartext source"
+except ImportError as e:
+    gpgme_support = False
+    decrypt_samba_gpg_help = "Decrypt the SambaGPG password not supported, " + \
+            "python-gpgme required"
+
+disabled_virtual_attributes = {
+    }
+
+virtual_attributes = {
+    "virtualClearTextUTF8": {
+        "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
+        },
+    "virtualClearTextUTF16": {
+        "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
+        },
+    "virtualSambaGPG": {
+        "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
+        },
+    }
+
+get_random_bytes_fn = None
+if get_random_bytes_fn is None:
+    try:
+        import Crypto.Random
+        get_random_bytes_fn = Crypto.Random.get_random_bytes
+    except ImportError as e:
+        pass
+if get_random_bytes_fn is None:
+    try:
+        import M2Crypto.Rand
+        get_random_bytes_fn = M2Crypto.Rand.rand_bytes
+    except ImportError as e:
+        pass
+
+def check_random():
+    if get_random_bytes_fn is not None:
+        return None
+    return "Crypto.Random or M2Crypto.Rand required"
+
+def get_random_bytes(num):
+    random_reason = check_random()
+    if random_reason is not None:
+        raise ImportError(random_reason)
+    return get_random_bytes_fn(num)
+
+def get_crypt_value(alg, utf8pw, rounds=0):
+    algs = {
+        "5": {"length": 43},
+        "6": {"length": 86},
+    }
+    assert alg in algs
+    salt = get_random_bytes(16)
+    # The salt needs to be in [A-Za-z0-9./]
+    # base64 is close enough and as we had 16
+    # random bytes but only need 16 characters
+    # we can ignore the possible == at the end
+    # of the base64 string
+    # we just need to replace '+' by '.'
+    b64salt = base64.b64encode(salt)[0:16].replace('+', '.')
+    crypt_salt = ""
+    if rounds != 0:
+        crypt_salt = "$%s$rounds=%s$%s$" % (alg, rounds, b64salt)
+    else:
+        crypt_salt = "$%s$%s$" % (alg, b64salt)
+
+    crypt_value = crypt.crypt(utf8pw, crypt_salt)
+    if crypt_value is None:
+        raise NotImplementedError("crypt.crypt(%s) returned None" % (crypt_salt))
+    expected_len = len(crypt_salt) + algs[alg]["length"]
+    if len(crypt_value) != expected_len:
+        raise NotImplementedError("crypt.crypt(%s) returned a value with length %d, expected length is %d" % (
+            crypt_salt, len(crypt_value), expected_len))
+    return crypt_value
+
+# Extract the rounds value from the options of a virtualCrypt attribute
+# i.e. options = "rounds=20;other=ignored;" will return 20
+# if the rounds option is not found or the value is not a number, 0 is returned
+# which indicates that the default number of rounds should be used.
+def get_rounds(options):
+    if not options:
+        return 0
+
+    opts = options.split(';')
+    for o in opts:
+        if o.lower().startswith("rounds="):
+            (key, _, val) = o.partition('=')
+            try:
+                return int(val)
+            except ValueError:
+                return 0
+    return 0
+
+try:
+    random_reason = check_random()
+    if random_reason is not None:
+        raise ImportError(random_reason)
+    import hashlib
+    h = hashlib.sha1()
+    h = None
+    virtual_attributes["virtualSSHA"] = {
+        }
+except ImportError as e:
+    reason = "hashlib.sha1()"
+    if random_reason:
+        reason += " and " + random_reason
+    reason += " required"
+    disabled_virtual_attributes["virtualSSHA"] = {
+        "reason" : reason,
+        }
+
+for (alg, attr) in [("5", "virtualCryptSHA256"), ("6", "virtualCryptSHA512")]:
+    try:
+        random_reason = check_random()
+        if random_reason is not None:
+            raise ImportError(random_reason)
+        import crypt
+        v = get_crypt_value(alg, "")
+        v = None
+        virtual_attributes[attr] = {
+            }
+    except ImportError as e:
+        reason = "crypt"
+        if random_reason:
+            reason += " and " + random_reason
+        reason += " required"
+        disabled_virtual_attributes[attr] = {
+            "reason" : reason,
+            }
+    except NotImplementedError as e:
+        reason = "modern '$%s$' salt in crypt(3) required" % (alg)
+        disabled_virtual_attributes[attr] = {
+            "reason" : reason,
+            }
+
+# Add the wDigest virtual attributes, virtualWDigest01 to virtualWDigest29
+for x in range(1, 30):
+    virtual_attributes["virtualWDigest%02d" % x] = {}
+
+virtual_attributes_help  = "The attributes to display (comma separated). "
+virtual_attributes_help += "Possible supported virtual attributes: %s" % ", ".join(sorted(virtual_attributes.keys()))
+if len(disabled_virtual_attributes) != 0:
+    virtual_attributes_help += "Unsupported virtual attributes: %s" % ", ".join(sorted(disabled_virtual_attributes.keys()))
+
+class cmd_user_create(Command):
+    """Create a new user.
+
+This command creates a new user account in the Active Directory domain.  The username specified on the command is the sAMaccountName.
+
+User accounts may represent physical entities, such as people or may be used as service accounts for applications.  User accounts are also referred to as security principals and are assigned a security identifier (SID).
+
+A user account enables a user to logon to a computer and domain with an identity that can be authenticated.  To maximize security, each user should have their own unique user account and password.  A user's access to domain resources is based on permissions assigned to the user account.
+
+Unix (RFC2307) attributes may be added to the user account. Attributes taken from NSS are obtained on the local machine. Explicitly given values override values obtained from NSS. Configure 'idmap_ldb:use rfc2307 = Yes' to use these attributes for UID/GID mapping.
+
+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 user create User1 passw0rd --given-name=John --surname=Smith --must-change-at-next-login -H ldap://samba.samdom.example.com -Uadministrator%passw1rd
+
+Example1 shows how to create a new user in the domain against a remote LDAP server.  The -H parameter is used to specify the remote target server.  The -U option is used to pass the userid and password authorized to issue the command remotely.
+
+Example2:
+sudo samba-tool user create User2 passw2rd --given-name=Jane --surname=Doe --must-change-at-next-login
+
+Example2 shows how to create a new user in the domain against the local server.   sudo is used so a user may run the command as root.  In this example, after User2 is created, he/she will be forced to change their password when they logon.
+
+Example3:
+samba-tool user create User3 passw3rd --userou='OU=OrgUnit'
+
+Example3 shows how to create a new user in the OrgUnit organizational unit.
+
+Example4:
+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
+
+Example5 shows how to create an RFC2307/NIS domain enabled user account. If
+--nis-domain is set, then the other four parameters are mandatory.
+
+"""
+    synopsis = "%prog <username> [<password>] [options]"
+
+    takes_options = [
+        Option("-H", "--URL", help="LDB URL for database or target server", type=str,
+                metavar="URL", dest="H"),
+        Option("--must-change-at-next-login",
+                help="Force password to be changed on next login",
+                action="store_true"),
+        Option("--random-password",
+                help="Generate random password",
+                action="store_true"),
+        Option("--smartcard-required",
+                help="Require a smartcard for interactive logons",
+                action="store_true"),
+        Option("--use-username-as-cn",
+                help="Force use of username as user's CN",
+                action="store_true"),
+        Option("--userou",
+                help="DN of alternative location (without domainDN counterpart) to default CN=Users in which new user object will be created. E. g. 'OU=<OU name>'",
+                type=str),
+        Option("--surname", help="User's surname", type=str),
+        Option("--given-name", help="User's given name", type=str),
+        Option("--initials", help="User's initials", type=str),
+        Option("--profile-path", help="User's profile path", type=str),
+        Option("--script-path", help="User's logon script path", type=str),
+        Option("--home-drive", help="User's home drive letter", type=str),
+        Option("--home-directory", help="User's home directory path", type=str),
+        Option("--job-title", help="User's job title", type=str),
+        Option("--department", help="User's department", type=str),
+        Option("--company", help="User's company", type=str),
+        Option("--description", help="User's description", type=str),
+        Option("--mail-address", help="User's email address", type=str),
+        Option("--internet-address", help="User's home page", type=str),
+        Option("--telephone-number", help="User's phone number", type=str),
+        Option("--physical-delivery-office", help="User's office location", type=str),
+        Option("--rfc2307-from-nss",
+                help="Copy Unix user attributes from NSS (will be overridden by explicit UID/GID/GECOS/shell)",
+                action="store_true"),
+        Option("--nis-domain", help="User's Unix/RFC2307 NIS domain", type=str),
+        Option("--unix-home", help="User's Unix/RFC2307 home directory",
+                type=str),
+        Option("--uid", help="User's Unix/RFC2307 username", type=str),
+        Option("--uid-number", help="User's Unix/RFC2307 numeric UID", type=int),
+        Option("--gid-number", help="User's Unix/RFC2307 primary GID number", type=int),
+        Option("--gecos", help="User's Unix/RFC2307 GECOS field", type=str),
+        Option("--login-shell", help="User's Unix/RFC2307 login shell", type=str),
+    ]
+
+    takes_args = ["username", "password?"]
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "credopts": options.CredentialsOptions,
+        "versionopts": options.VersionOptions,
+        }
+
+    def run(self, username, password=None, credopts=None, sambaopts=None,
+            versionopts=None, H=None, must_change_at_next_login=False,
+            random_password=False, use_username_as_cn=False, userou=None,
+            surname=None, given_name=None, initials=None, profile_path=None,
+            script_path=None, home_drive=None, home_directory=None,
+            job_title=None, department=None, company=None, description=None,
+            mail_address=None, internet_address=None, telephone_number=None,
+            physical_delivery_office=None, rfc2307_from_nss=False,
+            nis_domain=None, unix_home=None, uid=None, uid_number=None,
+            gid_number=None, gecos=None, login_shell=None,
+            smartcard_required=False):
+
+        if smartcard_required:
+            if password is not None and password is not '':
+                raise CommandError('It is not allowed to specify '
+                                   '--newpassword '
+                                   'together with --smartcard-required.')
+            if must_change_at_next_login:
+                raise CommandError('It is not allowed to specify '
+                                   '--must-change-at-next-login '
+                                   'together with --smartcard-required.')
+
+        if random_password and not smartcard_required:
+            password = generate_random_password(128, 255)
+
+        while True:
+            if smartcard_required:
+                break
+            if password is not None and password is not '':
+                break
+            password = getpass("New Password: ")
+            passwordverify = getpass("Retype Password: ")
+            if not password == passwordverify:
+                password = None
+                self.outf.write("Sorry, passwords do not match.\n")
+
+        if rfc2307_from_nss:
+                pwent = pwd.getpwnam(username)
+                if uid is None:
+                    uid = username
+                if uid_number is None:
+                    uid_number = pwent[2]
+                if gid_number is None:
+                    gid_number = pwent[3]
+                if gecos is None:
+                    gecos = pwent[4]
+                if login_shell is None:
+                    login_shell = pwent[6]
+
+        lp = sambaopts.get_loadparm()
+        creds = credopts.get_credentials(lp)
+
+        if uid_number or gid_number:
+            if not lp.get("idmap_ldb:use rfc2307"):
+                self.outf.write("You are setting a Unix/RFC2307 UID or GID. You may want to set 'idmap_ldb:use rfc2307 = Yes' to use those attributes for XID/SID-mapping.\n")
+
+        if nis_domain is not None:
+            if None in (uid_number, login_shell, unix_home, gid_number):
+                raise CommandError('Missing parameters. To enable NIS features, '
+                                   'the following options have to be given: '
+                                   '--nis-domain=, --uidNumber=, --login-shell='
+                                   ', --unix-home=, --gid-number= Operation '
+                                   'cancelled.')
+
+        try:
+            samdb = SamDB(url=H, session_info=system_session(),
+                          credentials=creds, lp=lp)
+            samdb.newuser(username, password, force_password_change_at_next_login_req=must_change_at_next_login,
+                          useusernameascn=use_username_as_cn, userou=userou, surname=surname, givenname=given_name, initials=initials,
+                          profilepath=profile_path, homedrive=home_drive, scriptpath=script_path, homedirectory=home_directory,
+                          jobtitle=job_title, department=department, company=company, description=description,
+                          mailaddress=mail_address, internetaddress=internet_address,
+                          telephonenumber=telephone_number, physicaldeliveryoffice=physical_delivery_office,
+                          nisdomain=nis_domain, unixhome=unix_home, uid=uid,
+                          uidnumber=uid_number, gidnumber=gid_number,
+                          gecos=gecos, loginshell=login_shell,
+                          smartcard_required=smartcard_required)
+        except Exception, e:
+            raise CommandError("Failed to add user '%s': " % username, e)
+
+        self.outf.write("User '%s' created successfully\n" % username)
+
+
+class cmd_user_add(cmd_user_create):
+    __doc__ = cmd_user_create.__doc__
+    # take this print out after the add subcommand is removed.
+    # the add subcommand is deprecated but left in for now to allow people to
+    # migrate to create
+
+    def run(self, *args, **kwargs):
+        self.outf.write(
+            "Note: samba-tool user add is deprecated.  "
+            "Please use samba-tool user create for the same function.\n")
+        return super(cmd_user_add, self).run(*args, **kwargs)
+
+
+class cmd_user_delete(Command):
+    """Delete a user.
+
+This command deletes a user account from the Active Directory domain.  The username specified on the command is the sAMAccountName.
+
+Once the account is deleted, all permissions and memberships associated with that account are deleted.  If a new user account is added with the same name as a previously deleted account name, the new user does not have the previous permissions.  The new account user will be assigned a new security identifier (SID) and permissions and memberships will have to be added.
+
+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 user delete User1 -H ldap://samba.samdom.example.com --username=administrator --password=passw1rd
+
+Example1 shows how to delete a user in the domain against a remote LDAP server.  The -H parameter is used to specify the remote target server.  The --username= and --password= options are used to pass the username and password of a user that exists on the remote server and is authorized to issue the command on that server.
+
+Example2:
+sudo samba-tool user delete User2
+
+Example2 shows how to delete a user in the domain against the local server.   sudo is used so a user may run the command as root.
+
+"""
+    synopsis = "%prog <username> [options]"
+
+    takes_options = [
+        Option("-H", "--URL", help="LDB URL for database or target server", type=str,
+               metavar="URL", dest="H"),
+    ]
+
+    takes_args = ["username"]
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "credopts": options.CredentialsOptions,
+        "versionopts": options.VersionOptions,
+        }
+
+    def run(self, username, credopts=None, sambaopts=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)
+
+        filter = ("(&(sAMAccountName=%s)(sAMAccountType=805306368))" %
+                   username)
+
+        try:
+            res = samdb.search(base=samdb.domain_dn(),
+                               scope=ldb.SCOPE_SUBTREE,
+                               expression=filter,
+                               attrs=["dn"])
+            user_dn = res[0].dn
+        except IndexError:
+            raise CommandError('Unable to find user "%s"' % (username))
+
+        try:
+            samdb.delete(user_dn)
+        except Exception, e:
+            raise CommandError('Failed to remove user "%s"' % username, e)
+        self.outf.write("Deleted user %s\n" % username)
+
+
+class cmd_user_list(Command):
+    """List all users."""
+
+    synopsis = "%prog [options]"
+
+    takes_options = [
+        Option("-H", "--URL", help="LDB URL for database or target server", type=str,
+               metavar="URL", dest="H"),
+        ]
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "credopts": options.CredentialsOptions,
+        "versionopts": options.VersionOptions,
+        }
+
+    def run(self, 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)
+
+        domain_dn = samdb.domain_dn()
+        res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
+                    expression=("(&(objectClass=user)(userAccountControl:%s:=%u))"
+                    % (ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT)),
+                    attrs=["samaccountname"])
+        if (len(res) == 0):
+            return
+
+        for msg in res:
+            self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
+
+
+class cmd_user_enable(Command):
+    """Enable an user.
+
+This command enables a user account for logon to an Active Directory domain.  The username specified on the command is the sAMAccountName.  The username may also be specified using the --filter option.
+
+There are many reasons why an account may become disabled.  These include:
+- If a user exceeds the account policy for logon attempts
+- If an administrator disables the account
+- If the account expires
+
+The samba-tool user enable command allows an administrator to enable an account which has become disabled.
+
+Additionally, the enable function allows an administrator to have a set of created user accounts defined and setup with default permissions that can be easily enabled for use.
+
+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 user enable Testuser1 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
+
+Example1 shows how to enable a user in the domain against a remote LDAP server.  The --URL parameter is used to specify the remote target server.  The --username= and --password= options are used to pass the username and password of a user that exists on the remote server and is authorized to update that server.
+
+Example2:
+su samba-tool user enable Testuser2
+
+Example2 shows how to enable user Testuser2 for use in the domain on the local server. sudo is used so a user may run the command as root.
+
+Example3:
+samba-tool user enable --filter=samaccountname=Testuser3
+
+Example3 shows how to enable a user in the domain against a local LDAP server.  It uses the --filter=samaccountname to specify the username.
+
+"""
+    synopsis = "%prog (<username>|--filter <filter>) [options]"
+
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "versionopts": options.VersionOptions,
+        "credopts": options.CredentialsOptions,
+    }
+
+    takes_options = [
+        Option("-H", "--URL", help="LDB URL for database or target server", type=str,
+               metavar="URL", dest="H"),
+        Option("--filter", help="LDAP Filter to set password on", type=str),
+        ]
+
+    takes_args = ["username?"]
+
+    def run(self, username=None, sambaopts=None, credopts=None,
+            versionopts=None, filter=None, H=None):
+        if username is None and filter is None:
+            raise CommandError("Either the username or '--filter' must be specified!")
+
+        if filter is None:
+            filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
+
+        lp = sambaopts.get_loadparm()
+        creds = credopts.get_credentials(lp, fallback_machine=True)
+
+        samdb = SamDB(url=H, session_info=system_session(),
+            credentials=creds, lp=lp)
+        try:
+            samdb.enable_account(filter)
+        except Exception, msg:
+            raise CommandError("Failed to enable user '%s': %s" % (username or filter, msg))
+        self.outf.write("Enabled user '%s'\n" % (username or filter))
+
+
+class cmd_user_disable(Command):
+    """Disable an user."""
+
+    synopsis = "%prog (<username>|--filter <filter>) [options]"
+
+    takes_options = [
+        Option("-H", "--URL", help="LDB URL for database or target server", type=str,
+               metavar="URL", dest="H"),
+        Option("--filter", help="LDAP Filter to set password on", type=str),
+        ]
+
+    takes_args = ["username?"]
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "credopts": options.CredentialsOptions,
+        "versionopts": options.VersionOptions,
+        }
+
+    def run(self, username=None, sambaopts=None, credopts=None,
+            versionopts=None, filter=None, H=None):
+        if username is None and filter is None:
+            raise CommandError("Either the username or '--filter' must be specified!")
+
+        if filter is None:
+            filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
+
+        lp = sambaopts.get_loadparm()
+        creds = credopts.get_credentials(lp, fallback_machine=True)
+
+        samdb = SamDB(url=H, session_info=system_session(),
+            credentials=creds, lp=lp)
+        try:
+            samdb.disable_account(filter)
+        except Exception, msg:
+            raise CommandError("Failed to disable user '%s': %s" % (username or filter, msg))
+
+
+class cmd_user_setexpiry(Command):
+    """Set the expiration of a user account.
+
+The user can either be specified by their sAMAccountName or using the --filter option.
+
+When a user account expires, it becomes disabled and the user is unable to logon.  The administrator may issue the samba-tool user enable command to enable the account for logon.  The permissions and memberships associated with the account are retained when the account is enabled.
+
+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 on a remote server.
+
+Example1:
+samba-tool user setexpiry User1 --days=20 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
+
+Example1 shows how to set the expiration of an account in a remote LDAP server.  The --URL parameter is used to specify the remote target server.  The --username= and --password= options are used to pass the username and password of a user that exists on the remote server and is authorized to update that server.
+
+Example2:
+su samba-tool user setexpiry User2
+
+Example2 shows how to set the account expiration of user User2 so it will never expire.  The user in this example resides on the  local server.   sudo is used so a user may run the command as root.
+
+Example3:
+samba-tool user setexpiry --days=20 --filter=samaccountname=User3
+
+Example3 shows how to set the account expiration date to end of day 20 days from the current day.  The username or sAMAccountName is specified using the --filter= parameter and the username in this example is User3.
+
+Example4:
+samba-tool user setexpiry --noexpiry User4
+Example4 shows how to set the account expiration so that it will never expire.  The username and sAMAccountName in this example is User4.
+
+"""
+    synopsis = "%prog (<username>|--filter <filter>) [options]"
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "versionopts": options.VersionOptions,
+        "credopts": options.CredentialsOptions,
+    }
+
+    takes_options = [
+        Option("-H", "--URL", help="LDB URL for database or target server", type=str,
+               metavar="URL", dest="H"),
+        Option("--filter", help="LDAP Filter to set password on", type=str),
+        Option("--days", help="Days to expiry", type=int, default=0),
+        Option("--noexpiry", help="Password does never expire", action="store_true", default=False),
+    ]
+
+    takes_args = ["username?"]
+
+    def run(self, username=None, sambaopts=None, credopts=None,
+            versionopts=None, H=None, filter=None, days=None, noexpiry=None):
+        if username is None and filter is None:
+            raise CommandError("Either the username or '--filter' must be specified!")
+
+        if filter is None:
+            filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
+
+        lp = sambaopts.get_loadparm()
+        creds = credopts.get_credentials(lp)
+
+        samdb = SamDB(url=H, session_info=system_session(),
+            credentials=creds, lp=lp)
+
+        try:
+            samdb.setexpiry(filter, days*24*3600, no_expiry_req=noexpiry)
+        except Exception, msg:
+            # FIXME: Catch more specific exception
+            raise CommandError("Failed to set expiry for user '%s': %s" % (
+                username or filter, msg))
+        if noexpiry:
+            self.outf.write("Expiry for user '%s' disabled.\n" % (
+                username or filter))
+        else:
+            self.outf.write("Expiry for user '%s' set to %u days.\n" % (
+                username or filter, days))
+
+
+class cmd_user_password(Command):
+    """Change password for a user account (the one provided in authentication).
+"""
+
+    synopsis = "%prog [options]"
+
+    takes_options = [
+        Option("--newpassword", help="New password", type=str),
+        ]
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "credopts": options.CredentialsOptions,
+        "versionopts": options.VersionOptions,
+        }
+
+    def run(self, credopts=None, sambaopts=None, versionopts=None,
+                newpassword=None):
+
+        lp = sambaopts.get_loadparm()
+        creds = credopts.get_credentials(lp)
+
+        # get old password now, to get the password prompts in the right order
+        old_password = creds.get_password()
+
+        net = Net(creds, lp, server=credopts.ipaddress)
+
+        password = newpassword
+        while True:
+            if password is not None and password is not '':
+                break
+            password = getpass("New Password: ")
+            passwordverify = getpass("Retype Password: ")
+            if not password == passwordverify:
+                password = None
+                self.outf.write("Sorry, passwords do not match.\n")
+
+        try:
+            net.change_password(password.encode('utf-8'))
+        except Exception, msg:
+            # FIXME: catch more specific exception
+            raise CommandError("Failed to change password : %s" % msg)
+        self.outf.write("Changed password OK\n")
+
+
+class cmd_user_setpassword(Command):
+    """Set or reset the password of a user account.
+
+This command sets or resets the logon password for a user account.  The username specified on the command is the sAMAccountName.  The username may also be specified using the --filter option.
+
+If the password is not specified on the command through the --newpassword parameter, the user is prompted for the password to be entered through the command line.
+
+It is good security practice for the administrator to use the --must-change-at-next-login option which requires that when the user logs on to the account for the first time following the password change, he/she must change the password.
+
+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 user setpassword TestUser1 --newpassword=passw0rd --URL=ldap://samba.samdom.example.com -Uadministrator%passw1rd
+
+Example1 shows how to set the password of user TestUser1 on a remote LDAP server.  The --URL parameter is used to specify the remote target server.  The -U option is used to pass the username and password of a user that exists on the remote server and is authorized to update the server.
+
+Example2:
+sudo samba-tool user setpassword TestUser2 --newpassword=passw0rd --must-change-at-next-login
+
+Example2 shows how an administrator would reset the TestUser2 user's password to passw0rd.  The user is running under the root userid using the sudo command.  In this example the user TestUser2 must change their password the next time they logon to the account.
+
+Example3:
+samba-tool user setpassword --filter=samaccountname=TestUser3 --newpassword=passw0rd
+
+Example3 shows how an administrator would reset TestUser3 user's password to passw0rd using the --filter= option to specify the username.
+
+"""
+    synopsis = "%prog (<username>|--filter <filter>) [options]"
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "versionopts": options.VersionOptions,
+        "credopts": options.CredentialsOptions,
+    }
+
+    takes_options = [
+        Option("-H", "--URL", help="LDB URL for database or target server", type=str,
+               metavar="URL", dest="H"),
+        Option("--filter", help="LDAP Filter to set password on", type=str),
+        Option("--newpassword", help="Set password", type=str),
+        Option("--must-change-at-next-login",
+               help="Force password to be changed on next login",
+               action="store_true"),
+        Option("--random-password",
+                help="Generate random password",
+                action="store_true"),
+        Option("--smartcard-required",
+                help="Require a smartcard for interactive logons",
+                action="store_true"),
+        Option("--clear-smartcard-required",
+                help="Don't require a smartcard for interactive logons",
+                action="store_true"),
+        ]
+
+    takes_args = ["username?"]
+
+    def run(self, username=None, filter=None, credopts=None, sambaopts=None,
+            versionopts=None, H=None, newpassword=None,
+            must_change_at_next_login=False, random_password=False,
+            smartcard_required=False, clear_smartcard_required=False):
+        if filter is None and username is None:
+            raise CommandError("Either the username or '--filter' must be specified!")
+
+        password = newpassword
+
+        if smartcard_required:
+            if password is not None and password is not '':
+                raise CommandError('It is not allowed to specify '
+                                   '--newpassword '
+                                   'together with --smartcard-required.')
+            if must_change_at_next_login:
+                raise CommandError('It is not allowed to specify '
+                                   '--must-change-at-next-login '
+                                   'together with --smartcard-required.')
+            if clear_smartcard_required:
+                raise CommandError('It is not allowed to specify '
+                                   '--clear-smartcard-required '
+                                   'together with --smartcard-required.')
+
+        if random_password and not smartcard_required:
+            password = generate_random_password(128, 255)
+
+        while True:
+            if smartcard_required:
+                break
+            if password is not None and password is not '':
+                break
+            password = getpass("New Password: ")
+            passwordverify = getpass("Retype Password: ")
+            if not password == passwordverify:
+                password = None
+                self.outf.write("Sorry, passwords do not match.\n")
+
+        if filter is None:
+            filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
+
+        lp = sambaopts.get_loadparm()
+        creds = credopts.get_credentials(lp)
+
+        creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
+
+        samdb = SamDB(url=H, session_info=system_session(),
+                      credentials=creds, lp=lp)
+
+        if smartcard_required:
+            command = ""
+            try:
+                command = "Failed to set UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
+                flags = dsdb.UF_SMARTCARD_REQUIRED
+                samdb.toggle_userAccountFlags(filter, flags, on=True)
+                command = "Failed to enable account for user '%s'" % (username or filter)
+                samdb.enable_account(filter)
+            except Exception, msg:
+                # FIXME: catch more specific exception
+                raise CommandError("%s: %s" % (command, msg))
+            self.outf.write("Added UF_SMARTCARD_REQUIRED OK\n")
+        else:
+            command = ""
+            try:
+                if clear_smartcard_required:
+                    command = "Failed to remove UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
+                    flags = dsdb.UF_SMARTCARD_REQUIRED
+                    samdb.toggle_userAccountFlags(filter, flags, on=False)
+                command = "Failed to set password for user '%s'" % (username or filter)
+                samdb.setpassword(filter, password,
+                                  force_change_at_next_login=must_change_at_next_login,
+                                  username=username)
+            except Exception, msg:
+                # FIXME: catch more specific exception
+                raise CommandError("%s: %s" % (command, msg))
+            self.outf.write("Changed password OK\n")
+
+class GetPasswordCommand(Command):
+
+    def __init__(self):
+        super(GetPasswordCommand, self).__init__()
+        self.lp = None
+
+    def connect_system_samdb(self, url, allow_local=False, verbose=False):
+
+        # using anonymous here, results in no authentication
+        # which means we can get system privileges via
+        # the privileged ldapi socket
+        creds = credentials.Credentials()
+        creds.set_anonymous()
+
+        if url is None and allow_local:
+            pass
+        elif url.lower().startswith("ldapi://"):
+            pass
+        elif url.lower().startswith("ldap://"):
+            raise CommandError("--url ldap:// is not supported for this command")
+        elif url.lower().startswith("ldaps://"):
+            raise CommandError("--url ldaps:// is not supported for this command")
+        elif not allow_local:
+            raise CommandError("--url requires an ldapi:// url for this command")
+
+        if verbose:
+            self.outf.write("Connecting to '%s'\n" % url)
+
+        samdb = SamDB(url=url, session_info=system_session(),
+                      credentials=creds, lp=self.lp)
+
+        try:
+            #
+            # Make sure we're connected as SYSTEM
+            #
+            res = samdb.search(base='', scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
+            assert len(res) == 1
+            sids = res[0].get("tokenGroups")
+            assert len(sids) == 1
+            sid = ndr_unpack(security.dom_sid, sids[0])
+            assert str(sid) == security.SID_NT_SYSTEM
+        except Exception as msg:
+            raise CommandError("You need to specify an URL that gives privileges as SID_NT_SYSTEM(%s)" %
+                               (security.SID_NT_SYSTEM))
+
+        # We use sort here in order to have a predictable processing order
+        # this might not be strictly needed, but also doesn't hurt here
+        for a in sorted(virtual_attributes.keys()):
+            flags = ldb.ATTR_FLAG_HIDDEN | virtual_attributes[a].get("flags", 0)
+            samdb.schema_attribute_add(a, flags, ldb.SYNTAX_OCTET_STRING)
+
+        return samdb
+
+    def get_account_attributes(self, samdb, username, basedn, filter, scope,
+                               attrs, decrypt):
+
+        raw_attrs = attrs[:]
+        search_attrs = []
+        attr_opts = {}
+        for a in raw_attrs:
+            (attr, _, opts) = a.partition(';')
+            if opts:
+                attr_opts[attr] = opts
+            else:
+                attr_opts[attr] = None
+            search_attrs.append(attr)
+        lower_attrs = [x.lower() for x in search_attrs]
+
+        require_supplementalCredentials = False
+        for a in virtual_attributes.keys():
+            if a.lower() in lower_attrs:
+                require_supplementalCredentials = True
+        add_supplementalCredentials = False
+        add_unicodePwd = False
+        if require_supplementalCredentials:
+            a = "supplementalCredentials"
+            if a.lower() not in lower_attrs:
+                search_attrs += [a]
+                add_supplementalCredentials = True
+            a = "unicodePwd"
+            if a.lower() not in lower_attrs:
+                search_attrs += [a]
+                add_unicodePwd = True
+        add_sAMAcountName = False
+        a = "sAMAccountName"
+        if a.lower() not in lower_attrs:
+            search_attrs += [a]
+            add_sAMAcountName = True
+
+        add_userPrincipalName = False
+        upn = "usePrincipalName"
+        if upn.lower() not in lower_attrs:
+            search_attrs += [upn]
+            add_userPrincipalName = True
+
+        if scope == ldb.SCOPE_BASE:
+            search_controls = ["show_deleted:1", "show_recycled:1"]
+        else:
+            search_controls = []
+        try:
+            res = samdb.search(base=basedn, expression=filter,
+                               scope=scope, attrs=search_attrs,
+                               controls=search_controls)
+            if len(res) == 0:
+                raise Exception('Unable to find user "%s"' % (username or filter))
+            if len(res) > 1:
+                raise Exception('Matched %u multiple users with filter "%s"' % (len(res), filter))
+        except Exception as msg:
+            # FIXME: catch more specific exception
+            raise CommandError("Failed to get password for user '%s': %s" % (username or filter, msg))
+        obj = res[0]
+
+        sc = None
+        unicodePwd = None
+        if "supplementalCredentials" in obj:
+            sc_blob = obj["supplementalCredentials"][0]
+            sc = ndr_unpack(drsblobs.supplementalCredentialsBlob, sc_blob)
+            if add_supplementalCredentials:
+                del obj["supplementalCredentials"]
+        if "unicodePwd" in obj:
+            unicodePwd = obj["unicodePwd"][0]
+            if add_unicodePwd:
+                del obj["unicodePwd"]
+        account_name = obj["sAMAccountName"][0]
+        if add_sAMAcountName:
+            del obj["sAMAccountName"]
+        if "userPrincipalName" in obj:
+            account_upn = obj["userPrincipalName"][0]
+        else:
+            realm = self.lp.get("realm")
+            account_upn = "%s@%s" % (account_name, realm.lower())
+        if add_userPrincipalName:
+            del obj["userPrincipalName"]
+
+        calculated = {}
+        def get_package(name, min_idx=0):
+            if name in calculated:
+                return calculated[name]
+            if sc is None:
+                return None
+            if min_idx < 0:
+                min_idx = len(sc.sub.packages) + min_idx
+            idx = 0
+            for p in sc.sub.packages:
+                idx += 1
+                if idx <= min_idx:
+                    continue
+                if name != p.name:
+                    continue
+
+                return binascii.a2b_hex(p.data)
+            return None
+
+        if decrypt:
+            #
+            # Samba adds 'Primary:SambaGPG' at the end.
+            # When Windows sets the password it keeps
+            # 'Primary:SambaGPG' and rotates it to
+            # the begining. So we can only use the value,
+            # if it is the last one.
+            #
+            # In order to get more protection we verify
+            # the nthash of the decrypted utf16 password
+            # against the stored nthash in unicodePwd.
+            #
+            sgv = get_package("Primary:SambaGPG", min_idx=-1)
+            if sgv is not None and unicodePwd is not None:
+                ctx = gpgme.Context()
+                ctx.armor = True
+                cipher_io = io.BytesIO(sgv)
+                plain_io = io.BytesIO()
+                try:
+                    ctx.decrypt(cipher_io, plain_io)
+                    cv = plain_io.getvalue()
+                    #
+                    # We only use the password if it matches
+                    # the current nthash stored in the unicodePwd
+                    # attribute
+                    #
+                    tmp = credentials.Credentials()
+                    tmp.set_anonymous()
+                    tmp.set_utf16_password(cv)
+                    nthash = tmp.get_nt_hash()
+                    if nthash == unicodePwd:
+                        calculated["Primary:CLEARTEXT"] = cv
+                except gpgme.GpgmeError as (major, minor, msg):
+                    if major == gpgme.ERR_BAD_SECKEY:
+                        msg = "ERR_BAD_SECKEY: " + msg
+                    else:
+                        msg = "MAJOR:%d, MINOR:%d: %s" % (major, minor, msg)
+                    self.outf.write("WARNING: '%s': SambaGPG can't be decrypted into CLEARTEXT: %s\n" % (
+                                    username or account_name, msg))
+
+        def get_utf8(a, b, username):
+            try:
+                u = unicode(b, 'utf-16-le')
+            except UnicodeDecodeError as e:
+                self.outf.write("WARNING: '%s': CLEARTEXT is invalid UTF-16-LE unable to generate %s\n" % (
+                                username, a))
+                return None
+            u8 = u.encode('utf-8')
+            return u8
+
+        # Extract the WDigest hash for the value specified by i.
+        # Builds an htdigest compatible value
+        DIGEST = "Digest"
+        def get_wDigest(i, primary_wdigest, account_name, account_upn,
+                        domain, dns_domain):
+            if i == 1:
+                user  = account_name
+                realm= domain
+            elif i == 2:
+                user  = account_name.lower()
+                realm = domain.lower()
+            elif i == 3:
+                user  = account_name.upper()
+                realm = domain.upper()
+            elif i == 4:
+                user  = account_name
+                realm = domain.upper()
+            elif i == 5:
+                user  = account_name
+                realm = domain.lower()
+            elif i == 6:
+                user  = account_name.upper()
+                realm = domain.lower()
+            elif i == 7:
+                user  = account_name.lower()
+                realm = domain.upper()
+            elif i == 8:
+                user  = account_name
+                realm = dns_domain.lower()
+            elif i == 9:
+                user  = account_name.lower()
+                realm = dns_domain.lower()
+            elif i == 10:
+                user  = account_name.upper()
+                realm = dns_domain.upper()
+            elif i == 11:
+                user  = account_name
+                realm = dns_domain.upper()
+            elif i == 12:
+                user  = account_name
+                realm = dns_domain.lower()
+            elif i == 13:
+                user  = account_name.upper()
+                realm = dns_domain.lower()
+            elif i == 14:
+                user  = account_name.lower()
+                realm = dns_domain.upper()
+            elif i == 15:
+                user  = account_upn
+                realm = ""
+            elif i == 16:
+                user  = account_upn.lower()
+                realm = ""
+            elif i == 17:
+                user  = account_upn.upper()
+                realm = ""
+            elif i == 18:
+                user  = "%s\\%s" % (domain, account_name)
+                realm = ""
+            elif i == 19:
+                user  = "%s\\%s" % (domain.lower(), account_name.lower())
+                realm = ""
+            elif i == 20:
+                user  = "%s\\%s" % (domain.upper(), account_name.upper())
+                realm = ""
+            elif i == 21:
+                user  = account_name
+                realm = DIGEST
+            elif i == 22:
+                user  = account_name.lower()
+                realm = DIGEST
+            elif i == 23:
+                user  = account_name.upper()
+                realm = DIGEST
+            elif i == 24:
+                user  = account_upn
+                realm = DIGEST
+            elif i == 25:
+                user  = account_upn.lower()
+                realm = DIGEST
+            elif i == 26:
+                user  = account_upn.upper()
+                realm = DIGEST
+            elif i == 27:
+                user  = "%s\\%s" % (domain, account_name)
+                realm = DIGEST
+            elif i == 28:
+                # Differs from spec, see tests
+                user  = "%s\\%s" % (domain.lower(), account_name.lower())
+                realm = DIGEST
+            elif i == 29:
+                # Differs from spec, see tests
+                user  = "%s\\%s" % (domain.upper(), account_name.upper())
+                realm = DIGEST
+            else:
+                user  = ""
+
+            digests = ndr_unpack(drsblobs.package_PrimaryWDigestBlob,
+                                 primary_wdigest)
+            try:
+                digest = binascii.hexlify(bytearray(digests.hashes[i-1].hash))
+                return "%s:%s:%s" % (user, realm, digest)
+            except IndexError:
+                return None
+
+
+        # get the value for a virtualCrypt attribute.
+        # look for an exact match on algorithm and rounds in supplemental creds
+        # if not found calculate using Primary:CLEARTEXT
+        # if no Primary:CLEARTEXT return the first supplementalCredential
+        #    that matches the algorithm.
+        def get_virtual_crypt_value(a, algorithm, rounds, username, account_name):
+            sv = None
+            fb = None
+            b = get_package("Primary:userPassword")
+            if b is not None:
+                (sv, fb) = get_userPassword_hash(b, algorithm, rounds)
+            if sv is None:
+                # No exact match on algorithm and number of rounds
+                # try and calculate one from the Primary:CLEARTEXT
+                b = get_package("Primary:CLEARTEXT")
+                if b is not None:
+                    u8 = get_utf8(a, b, username or account_name)
+                    if u8 is not None:
+                        sv = get_crypt_value(str(algorithm), u8, rounds)
+                if sv is None:
+                    # Unable to calculate a hash with the specified
+                    # number of rounds, fall back to the first hash using
+                    # the specified algorithm
+                    sv = fb
+            if sv is None:
+                return None
+            return "{CRYPT}" + sv
+
+        def get_userPassword_hash(blob, algorithm, rounds):
+            up = ndr_unpack(drsblobs.package_PrimaryUserPasswordBlob, blob)
+            SCHEME = "{CRYPT}"
+
+            # Check that the NT hash has not been changed without updating
+            # the user password hashes. This indicates that password has been
+            # changed without updating the supplemental credentials.
+            if unicodePwd != bytearray(up.current_nt_hash.hash):
+                return None
+
+            scheme_prefix = "$%d$" % algorithm
+            prefix = scheme_prefix
+            if rounds > 0:
+                prefix = "$%d$rounds=%d" % (algorithm, rounds)
+            scheme_match = None
+
+            for h in up.hashes:
+                if (scheme_match is None and
+                      h.scheme == SCHEME and
+                      h.value.startswith(scheme_prefix)):
+                    scheme_match = h.value
+                if h.scheme == SCHEME and h.value.startswith(prefix):
+                    return (h.value, scheme_match)
+
+            # No match on the number of rounds, return the value of the
+            # first matching scheme
+            return (None, scheme_match)
+
+        # We use sort here in order to have a predictable processing order
+        for a in sorted(virtual_attributes.keys()):
+            if not a.lower() in lower_attrs:
+                continue
+
+            if a == "virtualClearTextUTF8":
+                b = get_package("Primary:CLEARTEXT")
+                if b is None:
+                    continue
+                u8 = get_utf8(a, b, username or account_name)
+                if u8 is None:
+                    continue
+                v = u8
+            elif a == "virtualClearTextUTF16":
+                v = get_package("Primary:CLEARTEXT")
+                if v is None:
+                    continue
+            elif a == "virtualSSHA":
+                b = get_package("Primary:CLEARTEXT")
+                if b is None:
+                    continue
+                u8 = get_utf8(a, b, username or account_name)
+                if u8 is None:
+                    continue
+                salt = get_random_bytes(4)
+                h = hashlib.sha1()
+                h.update(u8)
+                h.update(salt)
+                bv = h.digest() + salt
+                v = "{SSHA}" + base64.b64encode(bv)
+            elif a == "virtualCryptSHA256":
+                rounds = get_rounds(attr_opts[a])
+                x = get_virtual_crypt_value(a, 5, rounds, username, account_name)
+                if x is None:
+                    continue
+                v = x
+            elif a == "virtualCryptSHA512":
+                rounds = get_rounds(attr_opts[a])
+                x = get_virtual_crypt_value(a, 6, rounds, username, account_name)
+                if x is None:
+                    continue
+                v = x
+            elif a == "virtualSambaGPG":
+                # Samba adds 'Primary:SambaGPG' at the end.
+                # When Windows sets the password it keeps
+                # 'Primary:SambaGPG' and rotates it to
+                # the begining. So we can only use the value,
+                # if it is the last one.
+                v = get_package("Primary:SambaGPG", min_idx=-1)
+                if v is None:
+                    continue
+            elif a.startswith("virtualWDigest"):
+                primary_wdigest = get_package("Primary:WDigest")
+                if primary_wdigest is None:
+                    continue
+                x = a[len("virtualWDigest"):]
+                try:
+                    i = int(x)
+                except ValueError:
+                    continue
+                domain = self.lp.get("workgroup")
+                dns_domain = samdb.domain_dns_name()
+                v = get_wDigest(i, primary_wdigest, account_name, account_upn, domain, dns_domain)
+                if v is None:
+                    continue
+            else:
+                continue
+            obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, a)
+        return obj
+
+    def parse_attributes(self, attributes):
+
+        if attributes is None:
+            raise CommandError("Please specify --attributes")
+        attrs = attributes.split(',')
+        password_attrs = []
+        for pa in attrs:
+            pa = pa.lstrip().rstrip()
+            for da in disabled_virtual_attributes.keys():
+                if pa.lower() == da.lower():
+                    r = disabled_virtual_attributes[da]["reason"]
+                    raise CommandError("Virtual attribute '%s' not supported: %s" % (
+                                       da, r))
+            for va in virtual_attributes.keys():
+                if pa.lower() == va.lower():
+                    # Take the real name
+                    pa = va
+                    break
+            password_attrs += [pa]
+
+        return password_attrs
+
+class cmd_user_getpassword(GetPasswordCommand):
+    """Get the password fields of a user/computer account.
+
+This command gets the logon password for a user/computer account.
+
+The username specified on the command is the sAMAccountName.
+The username may also be specified using the --filter option.
+
+The command must be run from the root user id or another authorized user id.
+The '-H' or '--URL' option only supports ldapi:// or [tdb://] and can be
+used to adjust the local path. By default tdb:// is used by default.
+
+The '--attributes' parameter takes a comma separated list of attributes,
+which will be printed or given to the script specified by '--script'. If a
+specified attribute is not available on an object it's silently omitted.
+All attributes defined in the schema (e.g. the unicodePwd attribute holds
+the NTHASH) and the following virtual attributes are possible (see --help
+for which virtual attributes are supported in your environment):
+
+   virtualClearTextUTF16: The raw cleartext as stored in the
+                          'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
+                          with '--decrypt-samba-gpg') buffer inside of the
+                          supplementalCredentials attribute. This typically
+                          contains valid UTF-16-LE, but may contain random
+                          bytes, e.g. for computer accounts.
+
+   virtualClearTextUTF8:  As virtualClearTextUTF16, but converted to UTF-8
+                          (only from valid UTF-16-LE)
+
+   virtualSSHA:           As virtualClearTextUTF8, but a salted SHA-1
+                          checksum, useful for OpenLDAP's '{SSHA}' algorithm.
+
+   virtualCryptSHA256:    As virtualClearTextUTF8, but a salted SHA256
+                          checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
+                          with a $5$... salt, see crypt(3) on modern systems.
+                          The number of rounds used to calculate the hash can
+                          also be specified. By appending ";rounds=x" to the
+                          attribute name i.e. virtualCryptSHA256;rounds=10000
+                          will calculate a SHA256 hash with 10,000 rounds.
+                          non numeric values for rounds are silently ignored
+                          The value is calculated as follows:
+                          1) If a value exists in 'Primary:userPassword' with
+                             the specified number of rounds it is returned.
+                          2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
+                             '--decrypt-samba-gpg'. Calculate a hash with
+                             the specified number of rounds
+                          3) Return the first CryptSHA256 value in
+                             'Primary:userPassword'
+
+
+   virtualCryptSHA512:    As virtualClearTextUTF8, but a salted SHA512
+                          checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
+                          with a $6$... salt, see crypt(3) on modern systems.
+                          The number of rounds used to calculate the hash can
+                          also be specified. By appending ";rounds=x" to the
+                          attribute name i.e. virtualCryptSHA512;rounds=10000
+                          will calculate a SHA512 hash with 10,000 rounds.
+                          non numeric values for rounds are silently ignored
+                          The value is calculated as follows:
+                          1) If a value exists in 'Primary:userPassword' with
+                             the specified number of rounds it is returned.
+                          2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
+                             '--decrypt-samba-gpg'. Calculate a hash with
+                             the specified number of rounds
+                          3) Return the first CryptSHA512 value in
+                             'Primary:userPassword'
+
+   virtualWDigestNN:      The individual hash values stored in
+                          'Primary:WDigest' where NN is the hash number in
+                          the range 01 to 29.
+                          NOTE: As at 22-05-2017 the documentation:
+                          3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
+                        https://msdn.microsoft.com/en-us/library/cc245680.aspx
+                          is incorrect
+
+   virtualSambaGPG:       The raw cleartext as stored in the
+                          'Primary:SambaGPG' buffer inside of the
+                          supplementalCredentials attribute.
+                          See the 'password hash gpg key ids' option in
+                          smb.conf.
+
+The '--decrypt-samba-gpg' option triggers decryption of the
+Primary:SambaGPG buffer. Check with '--help' if this feature is available
+in your environment or not (the python-gpgme package is required).  Please
+note that you might need to set the GNUPGHOME environment variable.  If the
+decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
+environment variable has been set correctly and the passphrase is already
+known by the gpg-agent.
+
+Example1:
+samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8
+
+Example2:
+samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-KeyVersionNumber,unicodePwd,virtualClearTextUTF16
+
+"""
+    def __init__(self):
+        super(cmd_user_getpassword, self).__init__()
+
+    synopsis = "%prog (<username>|--filter <filter>) [options]"
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "versionopts": options.VersionOptions,
+    }
+
+    takes_options = [
+        Option("-H", "--URL", help="LDB URL for sam.ldb database or local ldapi server", type=str,
+               metavar="URL", dest="H"),
+        Option("--filter", help="LDAP Filter to set password on", type=str),
+        Option("--attributes", type=str,
+               help=virtual_attributes_help,
+               metavar="ATTRIBUTELIST", dest="attributes"),
+        Option("--decrypt-samba-gpg",
+               help=decrypt_samba_gpg_help,
+               action="store_true", default=False, dest="decrypt_samba_gpg"),
+        ]
+
+    takes_args = ["username?"]
+
+    def run(self, username=None, H=None, filter=None,
+            attributes=None, decrypt_samba_gpg=None,
+            sambaopts=None, versionopts=None):
+        self.lp = sambaopts.get_loadparm()
+
+        if decrypt_samba_gpg and not gpgme_support:
+            raise CommandError(decrypt_samba_gpg_help)
+
+        if filter is None and username is None:
+            raise CommandError("Either the username or '--filter' must be specified!")
+
+        if filter is None:
+            filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
+
+        if attributes is None:
+            raise CommandError("Please specify --attributes")
+
+        password_attrs = self.parse_attributes(attributes)
+
+        samdb = self.connect_system_samdb(url=H, allow_local=True)
+
+        obj = self.get_account_attributes(samdb, username,
+                                          basedn=None,
+                                          filter=filter,
+                                          scope=ldb.SCOPE_SUBTREE,
+                                          attrs=password_attrs,
+                                          decrypt=decrypt_samba_gpg)
+
+        ldif = samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
+        self.outf.write("%s" % ldif)
+        self.outf.write("Got password OK\n")
+
+class cmd_user_syncpasswords(GetPasswordCommand):
+    """Sync the password of user accounts.
+
+This syncs logon passwords for user accounts.
+
+Note that this command should run on a single domain controller only
+(typically the PDC-emulator). However the "password hash gpg key ids"
+option should to be configured on all domain controllers.
+
+The command must be run from the root user id or another authorized user id.
+The '-H' or '--URL' option only supports ldapi:// and can be used to adjust the
+local path.  By default, ldapi:// is used with the default path to the
+privileged ldapi socket.
+
+This command has three modes: "Cache Initialization", "Sync Loop Run" and
+"Sync Loop Terminate".
+
+
+Cache Initialization
+====================
+
+The first time, this command needs to be called with
+'--cache-ldb-initialize' in order to initialize its cache.
+
+The cache initialization requires '--attributes' and allows the following
+optional options: '--decrypt-samba-gpg', '--script', '--filter' or
+'-H/--URL'.
+
+The '--attributes' parameter takes a comma separated list of attributes,
+which will be printed or given to the script specified by '--script'. If a
+specified attribute is not available on an object it will be silently omitted.
+All attributes defined in the schema (e.g. the unicodePwd attribute holds
+the NTHASH) and the following virtual attributes are possible (see '--help'
+for supported virtual attributes in your environment):
+
+   virtualClearTextUTF16: The raw cleartext as stored in the
+                          'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
+                          with '--decrypt-samba-gpg') buffer inside of the
+                          supplementalCredentials attribute. This typically
+                          contains valid UTF-16-LE, but may contain random
+                          bytes, e.g. for computer accounts.
+
+   virtualClearTextUTF8:  As virtualClearTextUTF16, but converted to UTF-8
+                          (only from valid UTF-16-LE)
+
+   virtualSSHA:           As virtualClearTextUTF8, but a salted SHA-1
+                          checksum, useful for OpenLDAP's '{SSHA}' algorithm.
+
+   virtualCryptSHA256:    As virtualClearTextUTF8, but a salted SHA256
+                          checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
+                          with a $5$... salt, see crypt(3) on modern systems.
+                          The number of rounds used to calculate the hash can
+                          also be specified. By appending ";rounds=x" to the
+                          attribute name i.e. virtualCryptSHA256;rounds=10000
+                          will calculate a SHA256 hash with 10,000 rounds.
+                          non numeric values for rounds are silently ignored
+                          The value is calculated as follows:
+                          1) If a value exists in 'Primary:userPassword' with
+                             the specified number of rounds it is returned.
+                          2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
+                             '--decrypt-samba-gpg'. Calculate a hash with
+                             the specified number of rounds
+                          3) Return the first CryptSHA256 value in
+                             'Primary:userPassword'
+
+   virtualCryptSHA512:    As virtualClearTextUTF8, but a salted SHA512
+                          checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
+                          with a $6$... salt, see crypt(3) on modern systems.
+                          The number of rounds used to calculate the hash can
+                          also be specified. By appending ";rounds=x" to the
+                          attribute name i.e. virtualCryptSHA512;rounds=10000
+                          will calculate a SHA512 hash with 10,000 rounds.
+                          non numeric values for rounds are silently ignored
+                          The value is calculated as follows:
+                          1) If a value exists in 'Primary:userPassword' with
+                             the specified number of rounds it is returned.
+                          2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
+                             '--decrypt-samba-gpg'. Calculate a hash with
+                             the specified number of rounds
+                          3) Return the first CryptSHA512 value in
+                             'Primary:userPassword'
+
+   virtualWDigestNN:      The individual hash values stored in
+                          'Primary:WDigest' where NN is the hash number in
+                          the range 01 to 29.
+                          NOTE: As at 22-05-2017 the documentation:
+                          3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
+                        https://msdn.microsoft.com/en-us/library/cc245680.aspx
+                          is incorrect.
+
+   virtualSambaGPG:       The raw cleartext as stored in the
+                          'Primary:SambaGPG' buffer inside of the
+                          supplementalCredentials attribute.
+                          See the 'password hash gpg key ids' option in
+                          smb.conf.
+
+The '--decrypt-samba-gpg' option triggers decryption of the
+Primary:SambaGPG buffer. Check with '--help' if this feature is available
+in your environment or not (the python-gpgme package is required).  Please
+note that you might need to set the GNUPGHOME environment variable.  If the
+decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
+environment variable has been set correctly and the passphrase is already
+known by the gpg-agent.
+
+The '--script' option specifies a custom script that is called whenever any
+of the dirsyncAttributes (see below) was changed. The script is called
+without any arguments. It gets the LDIF for exactly one object on STDIN.
+If the script processed the object successfully it has to respond with a
+single line starting with 'DONE-EXIT: ' followed by an optional message.
+
+Note that the script might be called without any password change, e.g. if
+the account was disabled (an userAccountControl change) or the
+sAMAccountName was changed. The objectGUID,isDeleted,isRecycled attributes
+are always returned as unique identifier of the account. It might be useful
+to also ask for non-password attributes like: objectSid, sAMAccountName,
+userPrincipalName, userAccountControl, pwdLastSet and msDS-KeyVersionNumber.
+Depending on the object, some attributes may not be present/available,
+but you always get the current state (and not a diff).
+
+If no '--script' option is specified, the LDIF will be printed on STDOUT or
+into the logfile.
+
+The default filter for the LDAP_SERVER_DIRSYNC_OID search is:
+(&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=512)\\
+    (!(sAMAccountName=krbtgt*)))
+This means only normal (non-krbtgt) user
+accounts are monitored.  The '--filter' can modify that, e.g. if it's
+required to also sync computer accounts.
+
+
+Sync Loop Run
+=============
+
+This (default) mode runs in an endless loop waiting for password related
+changes in the active directory database. It makes use of the
+LDAP_SERVER_DIRSYNC_OID and LDAP_SERVER_NOTIFICATION_OID controls in order
+get changes in a reliable fashion. Objects are monitored for changes of the
+following dirsyncAttributes:
+
+  unicodePwd, dBCSPwd, supplementalCredentials, pwdLastSet, sAMAccountName,
+  userPrincipalName and userAccountControl.
+
+It recovers from LDAP disconnects and updates the cache in conservative way
+(in single steps after each succesfully processed change).  An error from
+the script (specified by '--script') will result in fatal error and this
+command will exit.  But the cache state should be still valid and can be
+resumed in the next "Sync Loop Run".
+
+The '--logfile' option specifies an optional (required if '--daemon' is
+specified) logfile that takes all output of the command. The logfile is
+automatically reopened if fstat returns st_nlink == 0.
+
+The optional '--daemon' option will put the command into the background.
+
+You can stop the command without the '--daemon' option, also by hitting
+strg+c.
+
+If you specify the '--no-wait' option the command skips the
+LDAP_SERVER_NOTIFICATION_OID 'waiting' step and exit once
+all LDAP_SERVER_DIRSYNC_OID changes are consumed.
+
+Sync Loop Terminate
+===================
+
+In order to terminate an already running command (likely as daemon) the
+'--terminate' option can be used. This also requires the '--logfile' option
+to be specified.
+
+
+Example1:
+samba-tool user syncpasswords --cache-ldb-initialize \\
+    --attributes=virtualClearTextUTF8
+samba-tool user syncpasswords
+
+Example2:
+samba-tool user syncpasswords --cache-ldb-initialize \\
+    --attributes=objectGUID,objectSID,sAMAccountName,\\
+    userPrincipalName,userAccountControl,pwdLastSet,\\
+    msDS-KeyVersionNumber,virtualCryptSHA512 \\
+    --script=/path/to/my-custom-syncpasswords-script.py
+samba-tool user syncpasswords --daemon \\
+    --logfile=/var/log/samba/user-syncpasswords.log
+samba-tool user syncpasswords --terminate \\
+    --logfile=/var/log/samba/user-syncpasswords.log
+
+"""
+    def __init__(self):
+        super(cmd_user_syncpasswords, self).__init__()
+
+    synopsis = "%prog [--cache-ldb-initialize] [options]"
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "versionopts": options.VersionOptions,
+    }
+
+    takes_options = [
+        Option("--cache-ldb-initialize",
+               help="Initialize the cache for the first time",
+               dest="cache_ldb_initialize", action="store_true"),
+        Option("--cache-ldb", help="optional LDB URL user-syncpasswords-cache.ldb", type=str,
+               metavar="CACHE-LDB-PATH", dest="cache_ldb"),
+        Option("-H", "--URL", help="optional LDB URL for a local ldapi server", type=str,
+               metavar="URL", dest="H"),
+        Option("--filter", help="optional LDAP filter to set password on", type=str,
+               metavar="LDAP-SEARCH-FILTER", dest="filter"),
+        Option("--attributes", type=str,
+               help=virtual_attributes_help,
+               metavar="ATTRIBUTELIST", dest="attributes"),
+        Option("--decrypt-samba-gpg",
+               help=decrypt_samba_gpg_help,
+               action="store_true", default=False, dest="decrypt_samba_gpg"),
+        Option("--script", help="Script that is called for each password change", type=str,
+               metavar="/path/to/syncpasswords.script", dest="script"),
+        Option("--no-wait", help="Don't block waiting for changes",
+               action="store_true", default=False, dest="nowait"),
+        Option("--logfile", type=str,
+               help="The logfile to use (required in --daemon mode).",
+               metavar="/path/to/syncpasswords.log", dest="logfile"),
+        Option("--daemon", help="daemonize after initial setup",
+               action="store_true", default=False, dest="daemon"),
+        Option("--terminate",
+               help="Send a SIGTERM to an already running (daemon) process",
+               action="store_true", default=False, dest="terminate"),
+        ]
+
+    def run(self, cache_ldb_initialize=False, cache_ldb=None,
+            H=None, filter=None,
+            attributes=None, decrypt_samba_gpg=None,
+            script=None, nowait=None, logfile=None, daemon=None, terminate=None,
+            sambaopts=None, versionopts=None):
+
+        self.lp = sambaopts.get_loadparm()
+        self.logfile = None
+        self.samdb_url = None
+        self.samdb = None
+        self.cache = None
+
+        if not cache_ldb_initialize:
+            if attributes is not None:
+                raise CommandError("--attributes is only allowed together with --cache-ldb-initialize")
+            if decrypt_samba_gpg:
+                raise CommandError("--decrypt-samba-gpg is only allowed together with --cache-ldb-initialize")
+            if script is not None:
+                raise CommandError("--script is only allowed together with --cache-ldb-initialize")
+            if filter is not None:
+                raise CommandError("--filter is only allowed together with --cache-ldb-initialize")
+            if H is not None:
+                raise CommandError("-H/--URL is only allowed together with --cache-ldb-initialize")
+        else:
+            if nowait is not False:
+                raise CommandError("--no-wait is not allowed together with --cache-ldb-initialize")
+            if logfile is not None:
+                raise CommandError("--logfile is not allowed together with --cache-ldb-initialize")
+            if daemon is not False:
+                raise CommandError("--daemon is not allowed together with --cache-ldb-initialize")
+            if terminate is not False:
+                raise CommandError("--terminate is not allowed together with --cache-ldb-initialize")
+
+        if nowait is True:
+            if daemon is True:
+                raise CommandError("--daemon is not allowed together with --no-wait")
+            if terminate is not False:
+                raise CommandError("--terminate is not allowed together with --no-wait")
+
+        if terminate is True and daemon is True:
+            raise CommandError("--terminate is not allowed together with --daemon")
+
+        if daemon is True and logfile is None:
+            raise CommandError("--daemon is only allowed together with --logfile")
+
+        if terminate is True and logfile is None:
+            raise CommandError("--terminate is only allowed together with --logfile")
+
+        if script is not None:
+            if not os.path.exists(script):
+                raise CommandError("script[%s] does not exist!" % script)
+
+            sync_command = "%s" % os.path.abspath(script)
+        else:
+            sync_command = None
+
+        dirsync_filter = filter
+        if dirsync_filter is None:
+            dirsync_filter = "(&" + \
+                               "(objectClass=user)" + \
+                               "(userAccountControl:%s:=%u)" % (
+                                ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \
+                               "(!(sAMAccountName=krbtgt*))" + \
+                             ")"
+
+        dirsync_secret_attrs = [
+            "unicodePwd",
+            "dBCSPwd",
+            "supplementalCredentials",
+        ]
+
+        dirsync_attrs = dirsync_secret_attrs + [
+            "pwdLastSet",
+            "sAMAccountName",
+            "userPrincipalName",
+            "userAccountControl",
+            "isDeleted",
+            "isRecycled",
+        ]
+
+        password_attrs = None
+
+        if cache_ldb_initialize:
+            if H is None:
+                H = "ldapi://%s" % os.path.abspath(self.lp.private_path("ldap_priv/ldapi"))
+
+            if decrypt_samba_gpg and not gpgme_support:
+                raise CommandError(decrypt_samba_gpg_help)
+
+            password_attrs = self.parse_attributes(attributes)
+            lower_attrs = [x.lower() for x in password_attrs]
+            # We always return these in order to track deletions
+            for a in ["objectGUID", "isDeleted", "isRecycled"]:
+                if a.lower() not in lower_attrs:
+                    password_attrs += [a]
+
+        if cache_ldb is not None:
+            if cache_ldb.lower().startswith("ldapi://"):
+                raise CommandError("--cache_ldb ldapi:// is not supported")
+            elif cache_ldb.lower().startswith("ldap://"):
+                raise CommandError("--cache_ldb ldap:// is not supported")
+            elif cache_ldb.lower().startswith("ldaps://"):
+                raise CommandError("--cache_ldb ldaps:// is not supported")
+            elif cache_ldb.lower().startswith("tdb://"):
+                pass
+            else:
+                if not os.path.exists(cache_ldb):
+                    cache_ldb = self.lp.private_path(cache_ldb)
+        else:
+            cache_ldb = self.lp.private_path("user-syncpasswords-cache.ldb")
+
+        self.lockfile = "%s.pid" % cache_ldb
+
+        def log_msg(msg):
+            if self.logfile is not None:
+                info = os.fstat(0)
+                if info.st_nlink == 0:
+                    logfile = self.logfile
+                    self.logfile = None
+                    log_msg("Closing logfile[%s] (st_nlink == 0)\n" % (logfile))
+                    logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0600)
+                    os.dup2(logfd, 0)
+                    os.dup2(logfd, 1)
+                    os.dup2(logfd, 2)
+                    os.close(logfd)
+                    log_msg("Reopened logfile[%s]\n" % (logfile))
+                    self.logfile = logfile
+            msg = "%s: pid[%d]: %s" % (
+                    time.ctime(),
+                    os.getpid(),
+                    msg)
+            self.outf.write(msg)
+            return
+
+        def load_cache():
+            cache_attrs = [
+                "samdbUrl",
+                "dirsyncFilter",
+                "dirsyncAttribute",
+                "dirsyncControl",
+                "passwordAttribute",
+                "decryptSambaGPG",
+                "syncCommand",
+                "currentPid",
+            ]
+
+            self.cache = Ldb(cache_ldb)
+            self.cache_dn = ldb.Dn(self.cache, "KEY=USERSYNCPASSWORDS")
+            res = self.cache.search(base=self.cache_dn, scope=ldb.SCOPE_BASE,
+                                    attrs=cache_attrs)
+            if len(res) == 1:
+                try:
+                    self.samdb_url = res[0]["samdbUrl"][0]
+                except KeyError as e:
+                    self.samdb_url = None
+            else:
+                self.samdb_url = None
+            if self.samdb_url is None and not cache_ldb_initialize:
+                raise CommandError("cache_ldb[%s] not initialized, use --cache-ldb-initialize the first time" % (
+                                   cache_ldb))
+            if self.samdb_url is not None and cache_ldb_initialize:
+                raise CommandError("cache_ldb[%s] already initialized, --cache-ldb-initialize not allowed" % (
+                                   cache_ldb))
+            if self.samdb_url is None:
+                self.samdb_url = H
+                self.dirsync_filter = dirsync_filter
+                self.dirsync_attrs = dirsync_attrs
+                self.dirsync_controls = ["dirsync:1:0:0","extended_dn:1:0"];
+                self.password_attrs = password_attrs
+                self.decrypt_samba_gpg = decrypt_samba_gpg
+                self.sync_command = sync_command
+                add_ldif  = "dn: %s\n" % self.cache_dn
+                add_ldif += "objectClass: userSyncPasswords\n"
+                add_ldif += "samdbUrl:: %s\n" % base64.b64encode(self.samdb_url)
+                add_ldif += "dirsyncFilter:: %s\n" % base64.b64encode(self.dirsync_filter)
+                for a in self.dirsync_attrs:
+                    add_ldif += "dirsyncAttribute:: %s\n" % base64.b64encode(a)
+                add_ldif += "dirsyncControl: %s\n" % self.dirsync_controls[0]
+                for a in self.password_attrs:
+                    add_ldif += "passwordAttribute:: %s\n" % base64.b64encode(a)
+                if self.decrypt_samba_gpg == True:
+                    add_ldif += "decryptSambaGPG: TRUE\n"
+                else:
+                    add_ldif += "decryptSambaGPG: FALSE\n"
+                if self.sync_command is not None:
+                    add_ldif += "syncCommand: %s\n" % self.sync_command
+                add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
+                self.cache.add_ldif(add_ldif)
+                self.current_pid = None
+                self.outf.write("Initialized cache_ldb[%s]\n" % (cache_ldb))
+                msgs = self.cache.parse_ldif(add_ldif)
+                changetype,msg = msgs.next()
+                ldif = self.cache.write_ldif(msg, ldb.CHANGETYPE_NONE)
+                self.outf.write("%s" % ldif)
+            else:
+                self.dirsync_filter = res[0]["dirsyncFilter"][0]
+                self.dirsync_attrs = []
+                for a in res[0]["dirsyncAttribute"]:
+                    self.dirsync_attrs.append(a)
+                self.dirsync_controls = [res[0]["dirsyncControl"][0], "extended_dn:1:0"]
+                self.password_attrs = []
+                for a in res[0]["passwordAttribute"]:
+                    self.password_attrs.append(a)
+                decrypt_string = res[0]["decryptSambaGPG"][0]
+                assert(decrypt_string in ["TRUE", "FALSE"])
+                if decrypt_string == "TRUE":
+                    self.decrypt_samba_gpg = True
+                else:
+                    self.decrypt_samba_gpg = False
+                if "syncCommand" in res[0]:
+                    self.sync_command = res[0]["syncCommand"][0]
+                else:
+                    self.sync_command = None
+                if "currentPid" in res[0]:
+                    self.current_pid = int(res[0]["currentPid"][0])
+                else:
+                    self.current_pid = None
+                log_msg("Using cache_ldb[%s]\n" % (cache_ldb))
+
+            return
+
+        def run_sync_command(dn, ldif):
+            log_msg("Call Popen[%s] for %s\n" % (self.sync_command, dn))
+            sync_command_p = Popen(self.sync_command,
+                                   stdin=PIPE,
+                                   stdout=PIPE,
+                                   stderr=STDOUT)
+
+            res = sync_command_p.poll()
+            assert res is None
+
+            input = "%s" % (ldif)
+            reply = sync_command_p.communicate(input)[0]
+            log_msg("%s\n" % (reply))
+            res = sync_command_p.poll()
+            if res is None:
+                sync_command_p.terminate()
+            res = sync_command_p.wait()
+
+            if reply.startswith("DONE-EXIT: "):
+                return
+
+            log_msg("RESULT: %s\n" % (res))
+            raise Exception("ERROR: %s - %s\n" % (res, reply))
+
+        def handle_object(idx, dirsync_obj):
+            binary_guid = dirsync_obj.dn.get_extended_component("GUID")
+            guid = ndr_unpack(misc.GUID, binary_guid)
+            binary_sid = dirsync_obj.dn.get_extended_component("SID")
+            sid = ndr_unpack(security.dom_sid, binary_sid)
+            domain_sid, rid = sid.split()
+            if rid == security.DOMAIN_RID_KRBTGT:
+                log_msg("# Dirsync[%d] SKIP: DOMAIN_RID_KRBTGT\n\n" % (idx))
+                return
+            for a in list(dirsync_obj.keys()):
+                for h in dirsync_secret_attrs:
+                    if a.lower() == h.lower():
+                        del dirsync_obj[a]
+                        dirsync_obj["# %s::" % a] = ["REDACTED SECRET ATTRIBUTE"]
+            dirsync_ldif = self.samdb.write_ldif(dirsync_obj, ldb.CHANGETYPE_NONE)
+            log_msg("# Dirsync[%d] %s %s\n%s" %(idx, guid, sid, dirsync_ldif))
+            obj = self.get_account_attributes(self.samdb,
+                                              username="%s" % sid,
+                                              basedn="<GUID=%s>" % guid,
+                                              filter="(objectClass=user)",
+                                              scope=ldb.SCOPE_BASE,
+                                              attrs=self.password_attrs,
+                                              decrypt=self.decrypt_samba_gpg)
+            ldif = self.samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
+            log_msg("# Passwords[%d] %s %s\n" % (idx, guid, sid))
+            if self.sync_command is None:
+                self.outf.write("%s" % (ldif))
+                return
+            self.outf.write("# attrs=%s\n" % (sorted(obj.keys())))
+            run_sync_command(obj.dn, ldif)
+
+        def check_current_pid_conflict(terminate):
+            flags = os.O_RDWR
+            if not terminate:
+                flags |= os.O_CREAT
+
+            try:
+                self.lockfd = os.open(self.lockfile, flags, 0600)
+            except IOError as (err, msg):
+                if err == errno.ENOENT:
+                    if terminate:
+                        return False
+                log_msg("check_current_pid_conflict: failed to open[%s] - %s (%d)" %
+                        (self.lockfile, msg, err))
+                raise
+
+            got_exclusive = False
+            try:
+                fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
+                got_exclusive = True
+            except IOError as (err, msg):
+                if err != errno.EACCES and err != errno.EAGAIN:
+                    log_msg("check_current_pid_conflict: failed to get exclusive lock[%s] - %s (%d)" %
+                            (self.lockfile, msg, err))
+                    raise
+
+            if not got_exclusive:
+                buf = os.read(self.lockfd, 64)
+                self.current_pid = None
+                try:
+                    self.current_pid = int(buf)
+                except ValueError as e:
+                    pass
+                if self.current_pid is not None:
+                    return True
+
+            if got_exclusive and terminate:
+                try:
+                    os.ftruncate(self.lockfd, 0)
+                except IOError as (err, msg):
+                    log_msg("check_current_pid_conflict: failed to truncate [%s] - %s (%d)" %
+                            (self.lockfile, msg, err))
+                    raise
+                os.close(self.lockfd)
+                self.lockfd = -1
+                return False
+
+            try:
+                fcntl.lockf(self.lockfd, fcntl.LOCK_SH)
+            except IOError as (err, msg):
+                log_msg("check_current_pid_conflict: failed to get shared lock[%s] - %s (%d)" %
+                        (self.lockfile, msg, err))
+
+            # We leave the function with the shared lock.
+            return False
+
+        def update_pid(pid):
+            if self.lockfd != -1:
+                got_exclusive = False
+                # Try 5 times to get the exclusiv lock.
+                for i in xrange(0, 5):
+                    try:
+                        fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
+                        got_exclusive = True
+                    except IOError as (err, msg):
+                        if err != errno.EACCES and err != errno.EAGAIN:
+                            log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s (%d)" %
+                                    (pid, self.lockfile, msg, err))
+                            raise
+                    if got_exclusive:
+                        break
+                    time.sleep(1)
+                if not got_exclusive:
+                    log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s" %
+                            (pid, self.lockfile))
+                    raise CommandError("update_pid(%r): failed to get exclusive lock[%s] after 5 seconds" %
+                                       (pid, self.lockfile))
+
+                if pid is not None:
+                    buf = "%d\n" % pid
+                else:
+                    buf = None
+                try:
+                    os.ftruncate(self.lockfd, 0)
+                    if buf is not None:
+                        os.write(self.lockfd, buf)
+                except IOError as (err, msg):
+                    log_msg("check_current_pid_conflict: failed to write pid to [%s] - %s (%d)" %
+                            (self.lockfile, msg, err))
+                    raise
+            self.current_pid = pid
+            if self.current_pid is not None:
+                log_msg("currentPid: %d\n" % self.current_pid)
+
+            modify_ldif =  "dn: %s\n" % (self.cache_dn)
+            modify_ldif += "changetype: modify\n"
+            modify_ldif += "replace: currentPid\n"
+            if self.current_pid is not None:
+                modify_ldif += "currentPid: %d\n" % (self.current_pid)
+            modify_ldif += "replace: currentTime\n"
+            modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
+            self.cache.modify_ldif(modify_ldif)
+            return
+
+        def update_cache(res_controls):
+            assert len(res_controls) > 0
+            assert res_controls[0].oid == "1.2.840.113556.1.4.841"
+            res_controls[0].critical = True
+            self.dirsync_controls = [str(res_controls[0]),"extended_dn:1:0"]
+            log_msg("dirsyncControls: %r\n" % self.dirsync_controls)
+
+            modify_ldif =  "dn: %s\n" % (self.cache_dn)
+            modify_ldif += "changetype: modify\n"
+            modify_ldif += "replace: dirsyncControl\n"
+            modify_ldif += "dirsyncControl: %s\n" % (self.dirsync_controls[0])
+            modify_ldif += "replace: currentTime\n"
+            modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
+            self.cache.modify_ldif(modify_ldif)
+            return
+
+        def check_object(dirsync_obj, res_controls):
+            assert len(res_controls) > 0
+            assert res_controls[0].oid == "1.2.840.113556.1.4.841"
+
+            binary_sid = dirsync_obj.dn.get_extended_component("SID")
+            sid = ndr_unpack(security.dom_sid, binary_sid)
+            dn = "KEY=%s" % sid
+            lastCookie = str(res_controls[0])
+
+            res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
+                                    expression="(lastCookie=%s)" % (
+                                        ldb.binary_encode(lastCookie)),
+                                    attrs=[])
+            if len(res) == 1:
+                return True
+            return False
+
+        def update_object(dirsync_obj, res_controls):
+            assert len(res_controls) > 0
+            assert res_controls[0].oid == "1.2.840.113556.1.4.841"
+
+            binary_sid = dirsync_obj.dn.get_extended_component("SID")
+            sid = ndr_unpack(security.dom_sid, binary_sid)
+            dn = "KEY=%s" % sid
+            lastCookie = str(res_controls[0])
+
+            self.cache.transaction_start()
+            try:
+                res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
+                                        expression="(objectClass=*)",
+                                        attrs=["lastCookie"])
+                if len(res) == 0:
+                    add_ldif  = "dn: %s\n" % (dn)
+                    add_ldif += "objectClass: userCookie\n"
+                    add_ldif += "lastCookie: %s\n" % (lastCookie)
+                    add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
+                    self.cache.add_ldif(add_ldif)
+                else:
+                    modify_ldif =  "dn: %s\n" % (dn)
+                    modify_ldif += "changetype: modify\n"
+                    modify_ldif += "replace: lastCookie\n"
+                    modify_ldif += "lastCookie: %s\n" % (lastCookie)
+                    modify_ldif += "replace: currentTime\n"
+                    modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
+                    self.cache.modify_ldif(modify_ldif)
+                self.cache.transaction_commit()
+            except Exception as e:
+                self.cache.transaction_cancel()
+
+            return
+
+        def dirsync_loop():
+            while True:
+                res = self.samdb.search(expression=self.dirsync_filter,
+                                        scope=ldb.SCOPE_SUBTREE,
+                                        attrs=self.dirsync_attrs,
+                                        controls=self.dirsync_controls)
+                log_msg("dirsync_loop(): results %d\n" % len(res))
+                ri = 0
+                for r in res:
+                    done = check_object(r, res.controls)
+                    if not done:
+                        handle_object(ri, r)
+                        update_object(r, res.controls)
+                    ri += 1
+                update_cache(res.controls)
+                if len(res) == 0:
+                    break
+
+        def sync_loop(wait):
+            notify_attrs = ["name", "uSNCreated", "uSNChanged", "objectClass"]
+            notify_controls = ["notification:1", "show_recycled:1"]
+            notify_handle = self.samdb.search_iterator(expression="objectClass=*",
+                                                       scope=ldb.SCOPE_SUBTREE,
+                                                       attrs=notify_attrs,
+                                                       controls=notify_controls,
+                                                       timeout=-1)
+
+            if wait is True:
+                log_msg("Resuming monitoring\n")
+            else:
+                log_msg("Getting changes\n")
+            self.outf.write("dirsyncFilter: %s\n" % self.dirsync_filter)
+            self.outf.write("dirsyncControls: %r\n" % self.dirsync_controls)
+            self.outf.write("syncCommand: %s\n" % self.sync_command)
+            dirsync_loop()
+
+            if wait is not True:
+                return
+
+            for msg in notify_handle:
+                if not isinstance(msg, ldb.Message):
+                    self.outf.write("referal: %s\n" % msg)
+                    continue
+                created = msg.get("uSNCreated")[0]
+                changed = msg.get("uSNChanged")[0]
+                log_msg("# Notify %s uSNCreated[%s] uSNChanged[%s]\n" %
+                        (msg.dn, created, changed))
+
+                dirsync_loop()
+
+            res = notify_handle.result()
+
+        def daemonize():
+            self.samdb = None
+            self.cache = None
+            orig_pid = os.getpid()
+            pid = os.fork()
+            if pid == 0:
+                os.setsid()
+                pid = os.fork()
+                if pid == 0: # Actual daemon
+                    pid = os.getpid()
+                    log_msg("Daemonized as pid %d (from %d)\n" % (pid, orig_pid))
+                    load_cache()
+                    return
+            os._exit(0)
+
+        if cache_ldb_initialize:
+            self.samdb_url = H
+            self.samdb = self.connect_system_samdb(url=self.samdb_url,
+                                                   verbose=True)
+            load_cache()
+            return
+
+        if logfile is not None:
+            import resource      # Resource usage information.
+            maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
+            if maxfd == resource.RLIM_INFINITY:
+                maxfd = 1024 # Rough guess at maximum number of open file descriptors.
+            logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0600)
+            self.outf.write("Using logfile[%s]\n" % logfile)
+            for fd in range(0, maxfd):
+                if fd == logfd:
+                    continue
+                try:
+                    os.close(fd)
+                except OSError:
+                    pass
+            os.dup2(logfd, 0)
+            os.dup2(logfd, 1)
+            os.dup2(logfd, 2)
+            os.close(logfd)
+            log_msg("Attached to logfile[%s]\n" % (logfile))
+            self.logfile = logfile
+
+        load_cache()
+        conflict = check_current_pid_conflict(terminate)
+        if terminate:
+            if self.current_pid is None:
+                log_msg("No process running.\n")
+                return
+            if not conflict:
+                log_msg("Proccess %d is not running anymore.\n" % (
+                        self.current_pid))
+                update_pid(None)
+                return
+            log_msg("Sending SIGTERM to proccess %d.\n" % (
+                    self.current_pid))
+            os.kill(self.current_pid, signal.SIGTERM)
+            return
+        if conflict:
+            raise CommandError("Exiting pid %d, command is already running as pid %d" % (
+                               os.getpid(), self.current_pid))
+
+        if daemon is True:
+            daemonize()
+        update_pid(os.getpid())
+
+        wait = True
+        while wait is True:
+            retry_sleep_min = 1
+            retry_sleep_max = 600
+            if nowait is True:
+                wait = False
+                retry_sleep = 0
+            else:
+                retry_sleep = retry_sleep_min
+
+            while self.samdb is None:
+                if retry_sleep != 0:
+                    log_msg("Wait before connect - sleep(%d)\n" % retry_sleep)
+                    time.sleep(retry_sleep)
+                retry_sleep = retry_sleep * 2
+                if retry_sleep >= retry_sleep_max:
+                    retry_sleep = retry_sleep_max
+                log_msg("Connecting to '%s'\n" % self.samdb_url)
+                try:
+                    self.samdb = self.connect_system_samdb(url=self.samdb_url)
+                except Exception as msg:
+                    self.samdb = None
+                    log_msg("Connect to samdb Exception => (%s)\n" % msg)
+                    if wait is not True:
+                        raise
+
+            try:
+                sync_loop(wait)
+            except ldb.LdbError as (enum, estr):
+                self.samdb = None
+                log_msg("ldb.LdbError(%d) => (%s)\n" % (enum, estr))
+
+        update_pid(None)
+        return
+
+class cmd_user_edit(Command):
+    """Modify User AD object.
+
+This command will allow editing of a user account in the Active Directory
+domain. You will then be able to add or change attributes and their values.
+
+The username 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 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.
+
+The -H parameter is used to specify the remote target server.
+
+Example2:
+samba-tool user edit User2
+
+Example2 shows how to edit a users attributes in the domain against a local
+LDAP server.
+
+Example3:
+samba-tool user edit User3 --editor=nano
+
+Example3 shows how to edit a users attributes in the domain against a local
+LDAP server using the 'nano' editor.
+
+"""
+    synopsis = "%prog <username> [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 = ["username"]
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "credopts": options.CredentialsOptions,
+        "versionopts": options.VersionOptions,
+        }
+
+    def run(self, username, credopts=None, sambaopts=None, versionopts=None,
+            H=None, editor=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)
+
+        filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
+                  (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
+
+        domaindn = samdb.domain_dn()
+
+        try:
+            res = samdb.search(base=domaindn,
+                               expression=filter,
+                               scope=ldb.SCOPE_SUBTREE)
+            user_dn = res[0].dn
+        except IndexError:
+            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)
+
+            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(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()
+
+        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)
+
+            self.outf.write("Modified User '%s' successfully\n" % username)
+
+class cmd_user(SuperCommand):
+    """User management."""
+
+    subcommands = {}
+    subcommands["add"] = cmd_user_add()
+    subcommands["create"] = cmd_user_create()
+    subcommands["delete"] = cmd_user_delete()
+    subcommands["disable"] = cmd_user_disable()
+    subcommands["enable"] = cmd_user_enable()
+    subcommands["list"] = cmd_user_list()
+    subcommands["setexpiry"] = cmd_user_setexpiry()
+    subcommands["password"] = cmd_user_password()
+    subcommands["setpassword"] = cmd_user_setpassword()
+    subcommands["getpassword"] = cmd_user_getpassword()
+    subcommands["syncpasswords"] = cmd_user_syncpasswords()
+    subcommands["edit"] = cmd_user_edit()
-- 
2.7.4


From 7c4bfee29a99ba60c70cb968a76a1fccdcb2b6cd Mon Sep 17 00:00:00 2001
From: Joe Guo <joeg at catalyst.net.nz>
Date: Thu, 1 Feb 2018 11:22:21 +1300
Subject: [PATCH 04/12] samba-tools/computer: replace user to computer

Simple text replacement, modify logic in following commits.

Signed-off-by: Joe Guo  <joeg at catalyst.net.nz>
---
 python/samba/netcmd/computer.py | 474 ++++++++++++++++++++--------------------
 1 file changed, 237 insertions(+), 237 deletions(-)

diff --git a/python/samba/netcmd/computer.py b/python/samba/netcmd/computer.py
index 3b744a3..91f2e89 100644
--- a/python/samba/netcmd/computer.py
+++ b/python/samba/netcmd/computer.py
@@ -1,4 +1,4 @@
-# user management
+# computer management
 #
 # Copyright Jelmer Vernooij 2010 <jelmer at samba.org>
 # Copyright Theresa Halloran 2011 <theresahalloran at gmail.com>
@@ -204,48 +204,48 @@ virtual_attributes_help += "Possible supported virtual attributes: %s" % ", ".jo
 if len(disabled_virtual_attributes) != 0:
     virtual_attributes_help += "Unsupported virtual attributes: %s" % ", ".join(sorted(disabled_virtual_attributes.keys()))
 
-class cmd_user_create(Command):
-    """Create a new user.
+class cmd_computer_create(Command):
+    """Create a new computer.
 
-This command creates a new user account in the Active Directory domain.  The username specified on the command is the sAMaccountName.
+This command creates a new computer account in the Active Directory domain.  The computername specified on the command is the sAMaccountName.
 
-User accounts may represent physical entities, such as people or may be used as service accounts for applications.  User accounts are also referred to as security principals and are assigned a security identifier (SID).
+Computer accounts may represent physical entities, such as people or may be used as service accounts for applications.  Computer accounts are also referred to as security principals and are assigned a security identifier (SID).
 
-A user account enables a user to logon to a computer and domain with an identity that can be authenticated.  To maximize security, each user should have their own unique user account and password.  A user's access to domain resources is based on permissions assigned to the user account.
+A computer account enables a computer to logon to a computer and domain with an identity that can be authenticated.  To maximize security, each computer should have their own unique computer account and password.  A computer's access to domain resources is based on permissions assigned to the computer account.
 
-Unix (RFC2307) attributes may be added to the user account. Attributes taken from NSS are obtained on the local machine. Explicitly given values override values obtained from NSS. Configure 'idmap_ldb:use rfc2307 = Yes' to use these attributes for UID/GID mapping.
+Unix (RFC2307) attributes may be added to the computer account. Attributes taken from NSS are obtained on the local machine. Explicitly given values override values obtained from NSS. Configure 'idmap_ldb:use rfc2307 = Yes' to use these attributes for UID/GID mapping.
 
 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 user create User1 passw0rd --given-name=John --surname=Smith --must-change-at-next-login -H ldap://samba.samdom.example.com -Uadministrator%passw1rd
+samba-tool computer create Computer1 passw0rd --given-name=John --surname=Smith --must-change-at-next-login -H ldap://samba.samdom.example.com -Uadministrator%passw1rd
 
-Example1 shows how to create a new user in the domain against a remote LDAP server.  The -H parameter is used to specify the remote target server.  The -U option is used to pass the userid and password authorized to issue the command remotely.
+Example1 shows how to create a new computer in the domain against a remote LDAP server.  The -H parameter is used to specify the remote target server.  The -U option is used to pass the userid and password authorized to issue the command remotely.
 
 Example2:
-sudo samba-tool user create User2 passw2rd --given-name=Jane --surname=Doe --must-change-at-next-login
+sudo samba-tool computer create Computer2 passw2rd --given-name=Jane --surname=Doe --must-change-at-next-login
 
-Example2 shows how to create a new user in the domain against the local server.   sudo is used so a user may run the command as root.  In this example, after User2 is created, he/she will be forced to change their password when they logon.
+Example2 shows how to create a new computer in the domain against the local server.   sudo is used so a computer may run the command as root.  In this example, after Computer2 is created, he/she will be forced to change their password when they logon.
 
 Example3:
-samba-tool user create User3 passw3rd --userou='OU=OrgUnit'
+samba-tool computer create Computer3 passw3rd --computerou='OU=OrgUnit'
 
-Example3 shows how to create a new user in the OrgUnit organizational unit.
+Example3 shows how to create a new computer in the OrgUnit organizational unit.
 
 Example4:
-samba-tool user create User4 passw4rd --rfc2307-from-nss --gecos 'some text'
+samba-tool computer create Computer4 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'.
+Example4 shows how to create a new computer 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 \
+samba-tool computer create Computer5 passw5rd --nis-domain=samdom --unix-home=/home/Computer5 \
            --uid-number=10005 --login-shell=/bin/false --gid-number=10000
 
-Example5 shows how to create an RFC2307/NIS domain enabled user account. If
+Example5 shows how to create an RFC2307/NIS domain enabled computer account. If
 --nis-domain is set, then the other four parameters are mandatory.
 
 """
-    synopsis = "%prog <username> [<password>] [options]"
+    synopsis = "%prog <computername> [<password>] [options]"
 
     takes_options = [
         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
@@ -259,41 +259,41 @@ Example5 shows how to create an RFC2307/NIS domain enabled user account. If
         Option("--smartcard-required",
                 help="Require a smartcard for interactive logons",
                 action="store_true"),
-        Option("--use-username-as-cn",
-                help="Force use of username as user's CN",
+        Option("--use-computername-as-cn",
+                help="Force use of computername as computer's CN",
                 action="store_true"),
-        Option("--userou",
-                help="DN of alternative location (without domainDN counterpart) to default CN=Users in which new user object will be created. E. g. 'OU=<OU name>'",
+        Option("--computerou",
+                help="DN of alternative location (without domainDN counterpart) to default CN=Computers in which new computer object will be created. E. g. 'OU=<OU name>'",
                 type=str),
-        Option("--surname", help="User's surname", type=str),
-        Option("--given-name", help="User's given name", type=str),
-        Option("--initials", help="User's initials", type=str),
-        Option("--profile-path", help="User's profile path", type=str),
-        Option("--script-path", help="User's logon script path", type=str),
-        Option("--home-drive", help="User's home drive letter", type=str),
-        Option("--home-directory", help="User's home directory path", type=str),
-        Option("--job-title", help="User's job title", type=str),
-        Option("--department", help="User's department", type=str),
-        Option("--company", help="User's company", type=str),
-        Option("--description", help="User's description", type=str),
-        Option("--mail-address", help="User's email address", type=str),
-        Option("--internet-address", help="User's home page", type=str),
-        Option("--telephone-number", help="User's phone number", type=str),
-        Option("--physical-delivery-office", help="User's office location", type=str),
+        Option("--surname", help="Computer's surname", type=str),
+        Option("--given-name", help="Computer's given name", type=str),
+        Option("--initials", help="Computer's initials", type=str),
+        Option("--profile-path", help="Computer's profile path", type=str),
+        Option("--script-path", help="Computer's logon script path", type=str),
+        Option("--home-drive", help="Computer's home drive letter", type=str),
+        Option("--home-directory", help="Computer's home directory path", type=str),
+        Option("--job-title", help="Computer's job title", type=str),
+        Option("--department", help="Computer's department", type=str),
+        Option("--company", help="Computer's company", type=str),
+        Option("--description", help="Computer's description", type=str),
+        Option("--mail-address", help="Computer's email address", type=str),
+        Option("--internet-address", help="Computer's home page", type=str),
+        Option("--telephone-number", help="Computer's phone number", type=str),
+        Option("--physical-delivery-office", help="Computer's office location", type=str),
         Option("--rfc2307-from-nss",
-                help="Copy Unix user attributes from NSS (will be overridden by explicit UID/GID/GECOS/shell)",
+                help="Copy Unix computer attributes from NSS (will be overridden by explicit UID/GID/GECOS/shell)",
                 action="store_true"),
-        Option("--nis-domain", help="User's Unix/RFC2307 NIS domain", type=str),
-        Option("--unix-home", help="User's Unix/RFC2307 home directory",
+        Option("--nis-domain", help="Computer's Unix/RFC2307 NIS domain", type=str),
+        Option("--unix-home", help="Computer's Unix/RFC2307 home directory",
                 type=str),
-        Option("--uid", help="User's Unix/RFC2307 username", type=str),
-        Option("--uid-number", help="User's Unix/RFC2307 numeric UID", type=int),
-        Option("--gid-number", help="User's Unix/RFC2307 primary GID number", type=int),
-        Option("--gecos", help="User's Unix/RFC2307 GECOS field", type=str),
-        Option("--login-shell", help="User's Unix/RFC2307 login shell", type=str),
+        Option("--uid", help="Computer's Unix/RFC2307 computername", type=str),
+        Option("--uid-number", help="Computer's Unix/RFC2307 numeric UID", type=int),
+        Option("--gid-number", help="Computer's Unix/RFC2307 primary GID number", type=int),
+        Option("--gecos", help="Computer's Unix/RFC2307 GECOS field", type=str),
+        Option("--login-shell", help="Computer's Unix/RFC2307 login shell", type=str),
     ]
 
-    takes_args = ["username", "password?"]
+    takes_args = ["computername", "password?"]
 
     takes_optiongroups = {
         "sambaopts": options.SambaOptions,
@@ -301,9 +301,9 @@ Example5 shows how to create an RFC2307/NIS domain enabled user account. If
         "versionopts": options.VersionOptions,
         }
 
-    def run(self, username, password=None, credopts=None, sambaopts=None,
+    def run(self, computername, password=None, credopts=None, sambaopts=None,
             versionopts=None, H=None, must_change_at_next_login=False,
-            random_password=False, use_username_as_cn=False, userou=None,
+            random_password=False, use_computername_as_cn=False, computerou=None,
             surname=None, given_name=None, initials=None, profile_path=None,
             script_path=None, home_drive=None, home_directory=None,
             job_title=None, department=None, company=None, description=None,
@@ -338,9 +338,9 @@ Example5 shows how to create an RFC2307/NIS domain enabled user account. If
                 self.outf.write("Sorry, passwords do not match.\n")
 
         if rfc2307_from_nss:
-                pwent = pwd.getpwnam(username)
+                pwent = pwd.getpwnam(computername)
                 if uid is None:
-                    uid = username
+                    uid = computername
                 if uid_number is None:
                     uid_number = pwent[2]
                 if gid_number is None:
@@ -368,8 +368,8 @@ Example5 shows how to create an RFC2307/NIS domain enabled user account. If
         try:
             samdb = SamDB(url=H, session_info=system_session(),
                           credentials=creds, lp=lp)
-            samdb.newuser(username, password, force_password_change_at_next_login_req=must_change_at_next_login,
-                          useusernameascn=use_username_as_cn, userou=userou, surname=surname, givenname=given_name, initials=initials,
+            samdb.newuser(computername, password, force_password_change_at_next_login_req=must_change_at_next_login,
+                          usecomputernameascn=use_computername_as_cn, computerou=computerou, surname=surname, givenname=given_name, initials=initials,
                           profilepath=profile_path, homedrive=home_drive, scriptpath=script_path, homedirectory=home_directory,
                           jobtitle=job_title, department=department, company=company, description=description,
                           mailaddress=mail_address, internetaddress=internet_address,
@@ -379,59 +379,59 @@ Example5 shows how to create an RFC2307/NIS domain enabled user account. If
                           gecos=gecos, loginshell=login_shell,
                           smartcard_required=smartcard_required)
         except Exception, e:
-            raise CommandError("Failed to add user '%s': " % username, e)
+            raise CommandError("Failed to add computer '%s': " % computername, e)
 
-        self.outf.write("User '%s' created successfully\n" % username)
+        self.outf.write("Computer '%s' created successfully\n" % computername)
 
 
-class cmd_user_add(cmd_user_create):
-    __doc__ = cmd_user_create.__doc__
+class cmd_computer_add(cmd_computer_create):
+    __doc__ = cmd_computer_create.__doc__
     # take this print out after the add subcommand is removed.
     # the add subcommand is deprecated but left in for now to allow people to
     # migrate to create
 
     def run(self, *args, **kwargs):
         self.outf.write(
-            "Note: samba-tool user add is deprecated.  "
-            "Please use samba-tool user create for the same function.\n")
-        return super(cmd_user_add, self).run(*args, **kwargs)
+            "Note: samba-tool computer add is deprecated.  "
+            "Please use samba-tool computer create for the same function.\n")
+        return super(cmd_computer_add, self).run(*args, **kwargs)
 
 
-class cmd_user_delete(Command):
-    """Delete a user.
+class cmd_computer_delete(Command):
+    """Delete a computer.
 
-This command deletes a user account from the Active Directory domain.  The username specified on the command is the sAMAccountName.
+This command deletes a computer account from the Active Directory domain.  The computername specified on the command is the sAMAccountName.
 
-Once the account is deleted, all permissions and memberships associated with that account are deleted.  If a new user account is added with the same name as a previously deleted account name, the new user does not have the previous permissions.  The new account user will be assigned a new security identifier (SID) and permissions and memberships will have to be added.
+Once the account is deleted, all permissions and memberships associated with that account are deleted.  If a new computer account is added with the same name as a previously deleted account name, the new computer does not have the previous permissions.  The new account computer will be assigned a new security identifier (SID) and permissions and memberships will have to be added.
 
 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 user delete User1 -H ldap://samba.samdom.example.com --username=administrator --password=passw1rd
+samba-tool computer delete Computer1 -H ldap://samba.samdom.example.com --computername=administrator --password=passw1rd
 
-Example1 shows how to delete a user in the domain against a remote LDAP server.  The -H parameter is used to specify the remote target server.  The --username= and --password= options are used to pass the username and password of a user that exists on the remote server and is authorized to issue the command on that server.
+Example1 shows how to delete a computer in the domain against a remote LDAP server.  The -H parameter is used to specify the remote target server.  The --computername= and --password= options are used to pass the computername and password of a computer that exists on the remote server and is authorized to issue the command on that server.
 
 Example2:
-sudo samba-tool user delete User2
+sudo samba-tool computer delete Computer2
 
-Example2 shows how to delete a user in the domain against the local server.   sudo is used so a user may run the command as root.
+Example2 shows how to delete a computer in the domain against the local server.   sudo is used so a computer may run the command as root.
 
 """
-    synopsis = "%prog <username> [options]"
+    synopsis = "%prog <computername> [options]"
 
     takes_options = [
         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
                metavar="URL", dest="H"),
     ]
 
-    takes_args = ["username"]
+    takes_args = ["computername"]
     takes_optiongroups = {
         "sambaopts": options.SambaOptions,
         "credopts": options.CredentialsOptions,
         "versionopts": options.VersionOptions,
         }
 
-    def run(self, username, credopts=None, sambaopts=None, versionopts=None,
+    def run(self, computername, credopts=None, sambaopts=None, versionopts=None,
             H=None):
         lp = sambaopts.get_loadparm()
         creds = credopts.get_credentials(lp, fallback_machine=True)
@@ -440,25 +440,25 @@ Example2 shows how to delete a user in the domain against the local server.   su
                       credentials=creds, lp=lp)
 
         filter = ("(&(sAMAccountName=%s)(sAMAccountType=805306368))" %
-                   username)
+                   computername)
 
         try:
             res = samdb.search(base=samdb.domain_dn(),
                                scope=ldb.SCOPE_SUBTREE,
                                expression=filter,
                                attrs=["dn"])
-            user_dn = res[0].dn
+            computer_dn = res[0].dn
         except IndexError:
-            raise CommandError('Unable to find user "%s"' % (username))
+            raise CommandError('Unable to find computer "%s"' % (computername))
 
         try:
-            samdb.delete(user_dn)
+            samdb.delete(computer_dn)
         except Exception, e:
-            raise CommandError('Failed to remove user "%s"' % username, e)
-        self.outf.write("Deleted user %s\n" % username)
+            raise CommandError('Failed to remove computer "%s"' % computername, e)
+        self.outf.write("Deleted computer %s\n" % computername)
 
 
-class cmd_user_list(Command):
+class cmd_computer_list(Command):
     """List all users."""
 
     synopsis = "%prog [options]"
@@ -483,7 +483,7 @@ class cmd_user_list(Command):
 
         domain_dn = samdb.domain_dn()
         res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
-                    expression=("(&(objectClass=user)(userAccountControl:%s:=%u))"
+                    expression=("(&(objectClass=computer)(userAccountControl:%s:=%u))"
                     % (ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT)),
                     attrs=["samaccountname"])
         if (len(res) == 0):
@@ -493,39 +493,39 @@ class cmd_user_list(Command):
             self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
 
 
-class cmd_user_enable(Command):
-    """Enable an user.
+class cmd_computer_enable(Command):
+    """Enable an computer.
 
-This command enables a user account for logon to an Active Directory domain.  The username specified on the command is the sAMAccountName.  The username may also be specified using the --filter option.
+This command enables a computer account for logon to an Active Directory domain.  The computername specified on the command is the sAMAccountName.  The computername may also be specified using the --filter option.
 
 There are many reasons why an account may become disabled.  These include:
-- If a user exceeds the account policy for logon attempts
+- If a computer exceeds the account policy for logon attempts
 - If an administrator disables the account
 - If the account expires
 
-The samba-tool user enable command allows an administrator to enable an account which has become disabled.
+The samba-tool computer enable command allows an administrator to enable an account which has become disabled.
 
-Additionally, the enable function allows an administrator to have a set of created user accounts defined and setup with default permissions that can be easily enabled for use.
+Additionally, the enable function allows an administrator to have a set of created computer accounts defined and setup with default permissions that can be easily enabled for use.
 
 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 user enable Testuser1 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
+samba-tool computer enable Testuser1 --URL=ldap://samba.samdom.example.com --computername=administrator --password=passw1rd
 
-Example1 shows how to enable a user in the domain against a remote LDAP server.  The --URL parameter is used to specify the remote target server.  The --username= and --password= options are used to pass the username and password of a user that exists on the remote server and is authorized to update that server.
+Example1 shows how to enable a computer in the domain against a remote LDAP server.  The --URL parameter is used to specify the remote target server.  The --computername= and --password= options are used to pass the computername and password of a computer that exists on the remote server and is authorized to update that server.
 
 Example2:
-su samba-tool user enable Testuser2
+su samba-tool computer enable Testuser2
 
-Example2 shows how to enable user Testuser2 for use in the domain on the local server. sudo is used so a user may run the command as root.
+Example2 shows how to enable computer Testuser2 for use in the domain on the local server. sudo is used so a computer may run the command as root.
 
 Example3:
-samba-tool user enable --filter=samaccountname=Testuser3
+samba-tool computer enable --filter=samaccountname=Testuser3
 
-Example3 shows how to enable a user in the domain against a local LDAP server.  It uses the --filter=samaccountname to specify the username.
+Example3 shows how to enable a computer in the domain against a local LDAP server.  It uses the --filter=samaccountname to specify the computername.
 
 """
-    synopsis = "%prog (<username>|--filter <filter>) [options]"
+    synopsis = "%prog (<computername>|--filter <filter>) [options]"
 
 
     takes_optiongroups = {
@@ -540,15 +540,15 @@ Example3 shows how to enable a user in the domain against a local LDAP server.
         Option("--filter", help="LDAP Filter to set password on", type=str),
         ]
 
-    takes_args = ["username?"]
+    takes_args = ["computername?"]
 
-    def run(self, username=None, sambaopts=None, credopts=None,
+    def run(self, computername=None, sambaopts=None, credopts=None,
             versionopts=None, filter=None, H=None):
-        if username is None and filter is None:
-            raise CommandError("Either the username or '--filter' must be specified!")
+        if computername is None and filter is None:
+            raise CommandError("Either the computername or '--filter' must be specified!")
 
         if filter is None:
-            filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
+            filter = "(&(objectClass=computer)(sAMAccountName=%s))" % (ldb.binary_encode(computername))
 
         lp = sambaopts.get_loadparm()
         creds = credopts.get_credentials(lp, fallback_machine=True)
@@ -558,14 +558,14 @@ Example3 shows how to enable a user in the domain against a local LDAP server.
         try:
             samdb.enable_account(filter)
         except Exception, msg:
-            raise CommandError("Failed to enable user '%s': %s" % (username or filter, msg))
-        self.outf.write("Enabled user '%s'\n" % (username or filter))
+            raise CommandError("Failed to enable computer '%s': %s" % (computername or filter, msg))
+        self.outf.write("Enabled computer '%s'\n" % (computername or filter))
 
 
-class cmd_user_disable(Command):
-    """Disable an user."""
+class cmd_computer_disable(Command):
+    """Disable an computer."""
 
-    synopsis = "%prog (<username>|--filter <filter>) [options]"
+    synopsis = "%prog (<computername>|--filter <filter>) [options]"
 
     takes_options = [
         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
@@ -573,7 +573,7 @@ class cmd_user_disable(Command):
         Option("--filter", help="LDAP Filter to set password on", type=str),
         ]
 
-    takes_args = ["username?"]
+    takes_args = ["computername?"]
 
     takes_optiongroups = {
         "sambaopts": options.SambaOptions,
@@ -581,13 +581,13 @@ class cmd_user_disable(Command):
         "versionopts": options.VersionOptions,
         }
 
-    def run(self, username=None, sambaopts=None, credopts=None,
+    def run(self, computername=None, sambaopts=None, credopts=None,
             versionopts=None, filter=None, H=None):
-        if username is None and filter is None:
-            raise CommandError("Either the username or '--filter' must be specified!")
+        if computername is None and filter is None:
+            raise CommandError("Either the computername or '--filter' must be specified!")
 
         if filter is None:
-            filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
+            filter = "(&(objectClass=computer)(sAMAccountName=%s))" % (ldb.binary_encode(computername))
 
         lp = sambaopts.get_loadparm()
         creds = credopts.get_credentials(lp, fallback_machine=True)
@@ -597,39 +597,39 @@ class cmd_user_disable(Command):
         try:
             samdb.disable_account(filter)
         except Exception, msg:
-            raise CommandError("Failed to disable user '%s': %s" % (username or filter, msg))
+            raise CommandError("Failed to disable computer '%s': %s" % (computername or filter, msg))
 
 
-class cmd_user_setexpiry(Command):
-    """Set the expiration of a user account.
+class cmd_computer_setexpiry(Command):
+    """Set the expiration of a computer account.
 
-The user can either be specified by their sAMAccountName or using the --filter option.
+The computer can either be specified by their sAMAccountName or using the --filter option.
 
-When a user account expires, it becomes disabled and the user is unable to logon.  The administrator may issue the samba-tool user enable command to enable the account for logon.  The permissions and memberships associated with the account are retained when the account is enabled.
+When a computer account expires, it becomes disabled and the computer is unable to logon.  The administrator may issue the samba-tool computer enable command to enable the account for logon.  The permissions and memberships associated with the account are retained when the account is enabled.
 
 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 on a remote server.
 
 Example1:
-samba-tool user setexpiry User1 --days=20 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
+samba-tool computer setexpiry Computer1 --days=20 --URL=ldap://samba.samdom.example.com --computername=administrator --password=passw1rd
 
-Example1 shows how to set the expiration of an account in a remote LDAP server.  The --URL parameter is used to specify the remote target server.  The --username= and --password= options are used to pass the username and password of a user that exists on the remote server and is authorized to update that server.
+Example1 shows how to set the expiration of an account in a remote LDAP server.  The --URL parameter is used to specify the remote target server.  The --computername= and --password= options are used to pass the computername and password of a computer that exists on the remote server and is authorized to update that server.
 
 Example2:
-su samba-tool user setexpiry User2
+su samba-tool computer setexpiry Computer2
 
-Example2 shows how to set the account expiration of user User2 so it will never expire.  The user in this example resides on the  local server.   sudo is used so a user may run the command as root.
+Example2 shows how to set the account expiration of computer Computer2 so it will never expire.  The computer in this example resides on the  local server.   sudo is used so a computer may run the command as root.
 
 Example3:
-samba-tool user setexpiry --days=20 --filter=samaccountname=User3
+samba-tool computer setexpiry --days=20 --filter=samaccountname=Computer3
 
-Example3 shows how to set the account expiration date to end of day 20 days from the current day.  The username or sAMAccountName is specified using the --filter= parameter and the username in this example is User3.
+Example3 shows how to set the account expiration date to end of day 20 days from the current day.  The computername or sAMAccountName is specified using the --filter= parameter and the computername in this example is Computer3.
 
 Example4:
-samba-tool user setexpiry --noexpiry User4
-Example4 shows how to set the account expiration so that it will never expire.  The username and sAMAccountName in this example is User4.
+samba-tool computer setexpiry --noexpiry Computer4
+Example4 shows how to set the account expiration so that it will never expire.  The computername and sAMAccountName in this example is Computer4.
 
 """
-    synopsis = "%prog (<username>|--filter <filter>) [options]"
+    synopsis = "%prog (<computername>|--filter <filter>) [options]"
 
     takes_optiongroups = {
         "sambaopts": options.SambaOptions,
@@ -645,15 +645,15 @@ Example4 shows how to set the account expiration so that it will never expire.
         Option("--noexpiry", help="Password does never expire", action="store_true", default=False),
     ]
 
-    takes_args = ["username?"]
+    takes_args = ["computername?"]
 
-    def run(self, username=None, sambaopts=None, credopts=None,
+    def run(self, computername=None, sambaopts=None, credopts=None,
             versionopts=None, H=None, filter=None, days=None, noexpiry=None):
-        if username is None and filter is None:
-            raise CommandError("Either the username or '--filter' must be specified!")
+        if computername is None and filter is None:
+            raise CommandError("Either the computername or '--filter' must be specified!")
 
         if filter is None:
-            filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
+            filter = "(&(objectClass=computer)(sAMAccountName=%s))" % (ldb.binary_encode(computername))
 
         lp = sambaopts.get_loadparm()
         creds = credopts.get_credentials(lp)
@@ -665,18 +665,18 @@ Example4 shows how to set the account expiration so that it will never expire.
             samdb.setexpiry(filter, days*24*3600, no_expiry_req=noexpiry)
         except Exception, msg:
             # FIXME: Catch more specific exception
-            raise CommandError("Failed to set expiry for user '%s': %s" % (
-                username or filter, msg))
+            raise CommandError("Failed to set expiry for computer '%s': %s" % (
+                computername or filter, msg))
         if noexpiry:
-            self.outf.write("Expiry for user '%s' disabled.\n" % (
-                username or filter))
+            self.outf.write("Expiry for computer '%s' disabled.\n" % (
+                computername or filter))
         else:
-            self.outf.write("Expiry for user '%s' set to %u days.\n" % (
-                username or filter, days))
+            self.outf.write("Expiry for computer '%s' set to %u days.\n" % (
+                computername or filter, days))
 
 
-class cmd_user_password(Command):
-    """Change password for a user account (the one provided in authentication).
+class cmd_computer_password(Command):
+    """Change password for a computer account (the one provided in authentication).
 """
 
     synopsis = "%prog [options]"
@@ -720,34 +720,34 @@ class cmd_user_password(Command):
         self.outf.write("Changed password OK\n")
 
 
-class cmd_user_setpassword(Command):
-    """Set or reset the password of a user account.
+class cmd_computer_setpassword(Command):
+    """Set or reset the password of a computer account.
 
-This command sets or resets the logon password for a user account.  The username specified on the command is the sAMAccountName.  The username may also be specified using the --filter option.
+This command sets or resets the logon password for a computer account.  The computername specified on the command is the sAMAccountName.  The computername may also be specified using the --filter option.
 
-If the password is not specified on the command through the --newpassword parameter, the user is prompted for the password to be entered through the command line.
+If the password is not specified on the command through the --newpassword parameter, the computer is prompted for the password to be entered through the command line.
 
-It is good security practice for the administrator to use the --must-change-at-next-login option which requires that when the user logs on to the account for the first time following the password change, he/she must change the password.
+It is good security practice for the administrator to use the --must-change-at-next-login option which requires that when the computer logs on to the account for the first time following the password change, he/she must change the password.
 
 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 user setpassword TestUser1 --newpassword=passw0rd --URL=ldap://samba.samdom.example.com -Uadministrator%passw1rd
+samba-tool computer setpassword TestComputer1 --newpassword=passw0rd --URL=ldap://samba.samdom.example.com -Uadministrator%passw1rd
 
-Example1 shows how to set the password of user TestUser1 on a remote LDAP server.  The --URL parameter is used to specify the remote target server.  The -U option is used to pass the username and password of a user that exists on the remote server and is authorized to update the server.
+Example1 shows how to set the password of computer TestComputer1 on a remote LDAP server.  The --URL parameter is used to specify the remote target server.  The -U option is used to pass the computername and password of a computer that exists on the remote server and is authorized to update the server.
 
 Example2:
-sudo samba-tool user setpassword TestUser2 --newpassword=passw0rd --must-change-at-next-login
+sudo samba-tool computer setpassword TestComputer2 --newpassword=passw0rd --must-change-at-next-login
 
-Example2 shows how an administrator would reset the TestUser2 user's password to passw0rd.  The user is running under the root userid using the sudo command.  In this example the user TestUser2 must change their password the next time they logon to the account.
+Example2 shows how an administrator would reset the TestComputer2 computer's password to passw0rd.  The computer is running under the root userid using the sudo command.  In this example the computer TestComputer2 must change their password the next time they logon to the account.
 
 Example3:
-samba-tool user setpassword --filter=samaccountname=TestUser3 --newpassword=passw0rd
+samba-tool computer setpassword --filter=samaccountname=TestComputer3 --newpassword=passw0rd
 
-Example3 shows how an administrator would reset TestUser3 user's password to passw0rd using the --filter= option to specify the username.
+Example3 shows how an administrator would reset TestComputer3 computer's password to passw0rd using the --filter= option to specify the computername.
 
 """
-    synopsis = "%prog (<username>|--filter <filter>) [options]"
+    synopsis = "%prog (<computername>|--filter <filter>) [options]"
 
     takes_optiongroups = {
         "sambaopts": options.SambaOptions,
@@ -774,14 +774,14 @@ Example3 shows how an administrator would reset TestUser3 user's password to pas
                 action="store_true"),
         ]
 
-    takes_args = ["username?"]
+    takes_args = ["computername?"]
 
-    def run(self, username=None, filter=None, credopts=None, sambaopts=None,
+    def run(self, computername=None, filter=None, credopts=None, sambaopts=None,
             versionopts=None, H=None, newpassword=None,
             must_change_at_next_login=False, random_password=False,
             smartcard_required=False, clear_smartcard_required=False):
-        if filter is None and username is None:
-            raise CommandError("Either the username or '--filter' must be specified!")
+        if filter is None and computername is None:
+            raise CommandError("Either the computername or '--filter' must be specified!")
 
         password = newpassword
 
@@ -814,7 +814,7 @@ Example3 shows how an administrator would reset TestUser3 user's password to pas
                 self.outf.write("Sorry, passwords do not match.\n")
 
         if filter is None:
-            filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
+            filter = "(&(objectClass=computer)(sAMAccountName=%s))" % (ldb.binary_encode(computername))
 
         lp = sambaopts.get_loadparm()
         creds = credopts.get_credentials(lp)
@@ -827,10 +827,10 @@ Example3 shows how an administrator would reset TestUser3 user's password to pas
         if smartcard_required:
             command = ""
             try:
-                command = "Failed to set UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
+                command = "Failed to set UF_SMARTCARD_REQUIRED for computer '%s'" % (computername or filter)
                 flags = dsdb.UF_SMARTCARD_REQUIRED
                 samdb.toggle_userAccountFlags(filter, flags, on=True)
-                command = "Failed to enable account for user '%s'" % (username or filter)
+                command = "Failed to enable account for computer '%s'" % (computername or filter)
                 samdb.enable_account(filter)
             except Exception, msg:
                 # FIXME: catch more specific exception
@@ -840,13 +840,13 @@ Example3 shows how an administrator would reset TestUser3 user's password to pas
             command = ""
             try:
                 if clear_smartcard_required:
-                    command = "Failed to remove UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
+                    command = "Failed to remove UF_SMARTCARD_REQUIRED for computer '%s'" % (computername or filter)
                     flags = dsdb.UF_SMARTCARD_REQUIRED
                     samdb.toggle_userAccountFlags(filter, flags, on=False)
-                command = "Failed to set password for user '%s'" % (username or filter)
+                command = "Failed to set password for computer '%s'" % (computername or filter)
                 samdb.setpassword(filter, password,
                                   force_change_at_next_login=must_change_at_next_login,
-                                  username=username)
+                                  computername=computername)
             except Exception, msg:
                 # FIXME: catch more specific exception
                 raise CommandError("%s: %s" % (command, msg))
@@ -905,7 +905,7 @@ class GetPasswordCommand(Command):
 
         return samdb
 
-    def get_account_attributes(self, samdb, username, basedn, filter, scope,
+    def get_account_attributes(self, samdb, computername, basedn, filter, scope,
                                attrs, decrypt):
 
         raw_attrs = attrs[:]
@@ -956,12 +956,12 @@ class GetPasswordCommand(Command):
                                scope=scope, attrs=search_attrs,
                                controls=search_controls)
             if len(res) == 0:
-                raise Exception('Unable to find user "%s"' % (username or filter))
+                raise Exception('Unable to find computer "%s"' % (computername or filter))
             if len(res) > 1:
                 raise Exception('Matched %u multiple users with filter "%s"' % (len(res), filter))
         except Exception as msg:
             # FIXME: catch more specific exception
-            raise CommandError("Failed to get password for user '%s': %s" % (username or filter, msg))
+            raise CommandError("Failed to get password for computer '%s': %s" % (computername or filter, msg))
         obj = res[0]
 
         sc = None
@@ -1043,14 +1043,14 @@ class GetPasswordCommand(Command):
                     else:
                         msg = "MAJOR:%d, MINOR:%d: %s" % (major, minor, msg)
                     self.outf.write("WARNING: '%s': SambaGPG can't be decrypted into CLEARTEXT: %s\n" % (
-                                    username or account_name, msg))
+                                    computername or account_name, msg))
 
-        def get_utf8(a, b, username):
+        def get_utf8(a, b, computername):
             try:
                 u = unicode(b, 'utf-16-le')
             except UnicodeDecodeError as e:
                 self.outf.write("WARNING: '%s': CLEARTEXT is invalid UTF-16-LE unable to generate %s\n" % (
-                                username, a))
+                                computername, a))
                 return None
             u8 = u.encode('utf-8')
             return u8
@@ -1166,7 +1166,7 @@ class GetPasswordCommand(Command):
         # if not found calculate using Primary:CLEARTEXT
         # if no Primary:CLEARTEXT return the first supplementalCredential
         #    that matches the algorithm.
-        def get_virtual_crypt_value(a, algorithm, rounds, username, account_name):
+        def get_virtual_crypt_value(a, algorithm, rounds, computername, account_name):
             sv = None
             fb = None
             b = get_package("Primary:userPassword")
@@ -1177,7 +1177,7 @@ class GetPasswordCommand(Command):
                 # try and calculate one from the Primary:CLEARTEXT
                 b = get_package("Primary:CLEARTEXT")
                 if b is not None:
-                    u8 = get_utf8(a, b, username or account_name)
+                    u8 = get_utf8(a, b, computername or account_name)
                     if u8 is not None:
                         sv = get_crypt_value(str(algorithm), u8, rounds)
                 if sv is None:
@@ -1226,7 +1226,7 @@ class GetPasswordCommand(Command):
                 b = get_package("Primary:CLEARTEXT")
                 if b is None:
                     continue
-                u8 = get_utf8(a, b, username or account_name)
+                u8 = get_utf8(a, b, computername or account_name)
                 if u8 is None:
                     continue
                 v = u8
@@ -1238,7 +1238,7 @@ class GetPasswordCommand(Command):
                 b = get_package("Primary:CLEARTEXT")
                 if b is None:
                     continue
-                u8 = get_utf8(a, b, username or account_name)
+                u8 = get_utf8(a, b, computername or account_name)
                 if u8 is None:
                     continue
                 salt = get_random_bytes(4)
@@ -1249,13 +1249,13 @@ class GetPasswordCommand(Command):
                 v = "{SSHA}" + base64.b64encode(bv)
             elif a == "virtualCryptSHA256":
                 rounds = get_rounds(attr_opts[a])
-                x = get_virtual_crypt_value(a, 5, rounds, username, account_name)
+                x = get_virtual_crypt_value(a, 5, rounds, computername, account_name)
                 if x is None:
                     continue
                 v = x
             elif a == "virtualCryptSHA512":
                 rounds = get_rounds(attr_opts[a])
-                x = get_virtual_crypt_value(a, 6, rounds, username, account_name)
+                x = get_virtual_crypt_value(a, 6, rounds, computername, account_name)
                 if x is None:
                     continue
                 v = x
@@ -1309,13 +1309,13 @@ class GetPasswordCommand(Command):
 
         return password_attrs
 
-class cmd_user_getpassword(GetPasswordCommand):
+class cmd_computer_getpassword(GetPasswordCommand):
     """Get the password fields of a user/computer account.
 
 This command gets the logon password for a user/computer account.
 
-The username specified on the command is the sAMAccountName.
-The username may also be specified using the --filter option.
+The computername specified on the command is the sAMAccountName.
+The computername may also be specified using the --filter option.
 
 The command must be run from the root user id or another authorized user id.
 The '-H' or '--URL' option only supports ldapi:// or [tdb://] and can be
@@ -1399,16 +1399,16 @@ environment variable has been set correctly and the passphrase is already
 known by the gpg-agent.
 
 Example1:
-samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8
+samba-tool computer getpassword TestComputer1 --attributes=pwdLastSet,virtualClearTextUTF8
 
 Example2:
-samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-KeyVersionNumber,unicodePwd,virtualClearTextUTF16
+samba-tool computer getpassword --filter=samaccountname=TestComputer3 --attributes=msDS-KeyVersionNumber,unicodePwd,virtualClearTextUTF16
 
 """
     def __init__(self):
-        super(cmd_user_getpassword, self).__init__()
+        super(cmd_computer_getpassword, self).__init__()
 
-    synopsis = "%prog (<username>|--filter <filter>) [options]"
+    synopsis = "%prog (<computername>|--filter <filter>) [options]"
 
     takes_optiongroups = {
         "sambaopts": options.SambaOptions,
@@ -1427,9 +1427,9 @@ samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-
                action="store_true", default=False, dest="decrypt_samba_gpg"),
         ]
 
-    takes_args = ["username?"]
+    takes_args = ["computername?"]
 
-    def run(self, username=None, H=None, filter=None,
+    def run(self, computername=None, H=None, filter=None,
             attributes=None, decrypt_samba_gpg=None,
             sambaopts=None, versionopts=None):
         self.lp = sambaopts.get_loadparm()
@@ -1437,11 +1437,11 @@ samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-
         if decrypt_samba_gpg and not gpgme_support:
             raise CommandError(decrypt_samba_gpg_help)
 
-        if filter is None and username is None:
-            raise CommandError("Either the username or '--filter' must be specified!")
+        if filter is None and computername is None:
+            raise CommandError("Either the computername or '--filter' must be specified!")
 
         if filter is None:
-            filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
+            filter = "(&(objectClass=computer)(sAMAccountName=%s))" % (ldb.binary_encode(computername))
 
         if attributes is None:
             raise CommandError("Please specify --attributes")
@@ -1450,7 +1450,7 @@ samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-
 
         samdb = self.connect_system_samdb(url=H, allow_local=True)
 
-        obj = self.get_account_attributes(samdb, username,
+        obj = self.get_account_attributes(samdb, computername,
                                           basedn=None,
                                           filter=filter,
                                           scope=ldb.SCOPE_SUBTREE,
@@ -1461,10 +1461,10 @@ samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-
         self.outf.write("%s" % ldif)
         self.outf.write("Got password OK\n")
 
-class cmd_user_syncpasswords(GetPasswordCommand):
-    """Sync the password of user accounts.
+class cmd_computer_syncpasswords(GetPasswordCommand):
+    """Sync the password of computer accounts.
 
-This syncs logon passwords for user accounts.
+This syncs logon passwords for computer accounts.
 
 Note that this command should run on a single domain controller only
 (typically the PDC-emulator). However the "password hash gpg key ids"
@@ -1584,9 +1584,9 @@ If no '--script' option is specified, the LDIF will be printed on STDOUT or
 into the logfile.
 
 The default filter for the LDAP_SERVER_DIRSYNC_OID search is:
-(&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=512)\\
+(&(objectClass=computer)(userAccountControl:1.2.840.113556.1.4.803:=512)\\
     (!(sAMAccountName=krbtgt*)))
-This means only normal (non-krbtgt) user
+This means only normal (non-krbtgt) computer
 accounts are monitored.  The '--filter' can modify that, e.g. if it's
 required to also sync computer accounts.
 
@@ -1631,24 +1631,24 @@ to be specified.
 
 
 Example1:
-samba-tool user syncpasswords --cache-ldb-initialize \\
+samba-tool computer syncpasswords --cache-ldb-initialize \\
     --attributes=virtualClearTextUTF8
-samba-tool user syncpasswords
+samba-tool computer syncpasswords
 
 Example2:
-samba-tool user syncpasswords --cache-ldb-initialize \\
+samba-tool computer syncpasswords --cache-ldb-initialize \\
     --attributes=objectGUID,objectSID,sAMAccountName,\\
     userPrincipalName,userAccountControl,pwdLastSet,\\
     msDS-KeyVersionNumber,virtualCryptSHA512 \\
     --script=/path/to/my-custom-syncpasswords-script.py
-samba-tool user syncpasswords --daemon \\
-    --logfile=/var/log/samba/user-syncpasswords.log
-samba-tool user syncpasswords --terminate \\
-    --logfile=/var/log/samba/user-syncpasswords.log
+samba-tool computer syncpasswords --daemon \\
+    --logfile=/var/log/samba/computer-syncpasswords.log
+samba-tool computer syncpasswords --terminate \\
+    --logfile=/var/log/samba/computer-syncpasswords.log
 
 """
     def __init__(self):
-        super(cmd_user_syncpasswords, self).__init__()
+        super(cmd_computer_syncpasswords, self).__init__()
 
     synopsis = "%prog [--cache-ldb-initialize] [options]"
 
@@ -1661,7 +1661,7 @@ samba-tool user syncpasswords --terminate \\
         Option("--cache-ldb-initialize",
                help="Initialize the cache for the first time",
                dest="cache_ldb_initialize", action="store_true"),
-        Option("--cache-ldb", help="optional LDB URL user-syncpasswords-cache.ldb", type=str,
+        Option("--cache-ldb", help="optional LDB URL computer-syncpasswords-cache.ldb", type=str,
                metavar="CACHE-LDB-PATH", dest="cache_ldb"),
         Option("-H", "--URL", help="optional LDB URL for a local ldapi server", type=str,
                metavar="URL", dest="H"),
@@ -1746,7 +1746,7 @@ samba-tool user syncpasswords --terminate \\
         dirsync_filter = filter
         if dirsync_filter is None:
             dirsync_filter = "(&" + \
-                               "(objectClass=user)" + \
+                               "(objectClass=computer)" + \
                                "(userAccountControl:%s:=%u)" % (
                                 ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \
                                "(!(sAMAccountName=krbtgt*))" + \
@@ -1796,7 +1796,7 @@ samba-tool user syncpasswords --terminate \\
                 if not os.path.exists(cache_ldb):
                     cache_ldb = self.lp.private_path(cache_ldb)
         else:
-            cache_ldb = self.lp.private_path("user-syncpasswords-cache.ldb")
+            cache_ldb = self.lp.private_path("computer-syncpasswords-cache.ldb")
 
         self.lockfile = "%s.pid" % cache_ldb
 
@@ -1949,9 +1949,9 @@ samba-tool user syncpasswords --terminate \\
             dirsync_ldif = self.samdb.write_ldif(dirsync_obj, ldb.CHANGETYPE_NONE)
             log_msg("# Dirsync[%d] %s %s\n%s" %(idx, guid, sid, dirsync_ldif))
             obj = self.get_account_attributes(self.samdb,
-                                              username="%s" % sid,
+                                              computername="%s" % sid,
                                               basedn="<GUID=%s>" % guid,
-                                              filter="(objectClass=user)",
+                                              filter="(objectClass=computer)",
                                               scope=ldb.SCOPE_BASE,
                                               attrs=self.password_attrs,
                                               decrypt=self.decrypt_samba_gpg)
@@ -2286,13 +2286,13 @@ samba-tool user syncpasswords --terminate \\
         update_pid(None)
         return
 
-class cmd_user_edit(Command):
-    """Modify User AD object.
+class cmd_computer_edit(Command):
+    """Modify Computer AD object.
 
-This command will allow editing of a user account in the Active Directory
+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 username specified on the command is the sAMAccountName.
+The computername specified on the command is the sAMAccountName.
 
 The command may be run from the root userid or another authorized userid.
 
@@ -2300,7 +2300,7 @@ 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 \
+samba-tool computer edit Computer1 -H ldap://samba.samdom.example.com \
 -U administrator --password=passw1rd
 
 Example1 shows how to edit a users attributes in the domain against a remote
@@ -2309,19 +2309,19 @@ LDAP server.
 The -H parameter is used to specify the remote target server.
 
 Example2:
-samba-tool user edit User2
+samba-tool computer edit Computer2
 
 Example2 shows how to edit a users attributes in the domain against a local
 LDAP server.
 
 Example3:
-samba-tool user edit User3 --editor=nano
+samba-tool computer edit Computer3 --editor=nano
 
 Example3 shows how to edit a users attributes in the domain against a local
 LDAP server using the 'nano' editor.
 
 """
-    synopsis = "%prog <username> [options]"
+    synopsis = "%prog <computername> [options]"
 
     takes_options = [
         Option("-H", "--URL", help="LDB URL for database or target server",
@@ -2330,14 +2330,14 @@ LDAP server using the 'nano' editor.
                " or 'vi' if no system default is set.", type=str),
     ]
 
-    takes_args = ["username"]
+    takes_args = ["computername"]
     takes_optiongroups = {
         "sambaopts": options.SambaOptions,
         "credopts": options.CredentialsOptions,
         "versionopts": options.VersionOptions,
         }
 
-    def run(self, username, credopts=None, sambaopts=None, versionopts=None,
+    def run(self, computername, credopts=None, sambaopts=None, versionopts=None,
             H=None, editor=None):
 
         lp = sambaopts.get_loadparm()
@@ -2346,7 +2346,7 @@ LDAP server using the 'nano' editor.
                       credentials=creds, lp=lp)
 
         filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
-                  (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
+                  (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(computername)))
 
         domaindn = samdb.domain_dn()
 
@@ -2354,9 +2354,9 @@ LDAP server using the 'nano' editor.
             res = samdb.search(base=domaindn,
                                expression=filter,
                                scope=ldb.SCOPE_SUBTREE)
-            user_dn = res[0].dn
+            computer_dn = res[0].dn
         except IndexError:
-            raise CommandError('Unable to find user "%s"' % (username))
+            raise CommandError('Unable to find computer "%s"' % (computername))
 
         for msg in res:
             r_ldif = samdb.write_ldif(msg, 1)
@@ -2391,47 +2391,47 @@ LDAP server using the 'nano' editor.
                     line = line[2:]
                     plus_lines.append(line)
 
-            user_ldif="dn: %s\n" % user_dn
-            user_ldif += "changetype: modify\n"
+            computer_ldif="dn: %s\n" % computer_dn
+            computer_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)
+                    computer_ldif += "delete: %s\n" % attr
+                    computer_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)
+                    computer_ldif += "replace: %s\n" % attr
+                    computer_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)
+                    computer_ldif += "add: %s\n" % attr
+                    computer_ldif += "%s: %s\n" % (attr, val)
 
             try:
-                samdb.modify_ldif(user_ldif)
+                samdb.modify_ldif(computer_ldif)
             except Exception as e:
-                raise CommandError("Failed to modify user '%s': " %
-                                   username, e)
+                raise CommandError("Failed to modify computer '%s': " %
+                                   computername, e)
 
-            self.outf.write("Modified User '%s' successfully\n" % username)
+            self.outf.write("Modified Computer '%s' successfully\n" % computername)
 
-class cmd_user(SuperCommand):
-    """User management."""
+class cmd_computer(SuperCommand):
+    """Computer management."""
 
     subcommands = {}
-    subcommands["add"] = cmd_user_add()
-    subcommands["create"] = cmd_user_create()
-    subcommands["delete"] = cmd_user_delete()
-    subcommands["disable"] = cmd_user_disable()
-    subcommands["enable"] = cmd_user_enable()
-    subcommands["list"] = cmd_user_list()
-    subcommands["setexpiry"] = cmd_user_setexpiry()
-    subcommands["password"] = cmd_user_password()
-    subcommands["setpassword"] = cmd_user_setpassword()
-    subcommands["getpassword"] = cmd_user_getpassword()
-    subcommands["syncpasswords"] = cmd_user_syncpasswords()
-    subcommands["edit"] = cmd_user_edit()
+    subcommands["add"] = cmd_computer_add()
+    subcommands["create"] = cmd_computer_create()
+    subcommands["delete"] = cmd_computer_delete()
+    subcommands["disable"] = cmd_computer_disable()
+    subcommands["enable"] = cmd_computer_enable()
+    subcommands["list"] = cmd_computer_list()
+    subcommands["setexpiry"] = cmd_computer_setexpiry()
+    subcommands["password"] = cmd_computer_password()
+    subcommands["setpassword"] = cmd_computer_setpassword()
+    subcommands["getpassword"] = cmd_computer_getpassword()
+    subcommands["syncpasswords"] = cmd_computer_syncpasswords()
+    subcommands["edit"] = cmd_computer_edit()
-- 
2.7.4


From 47ee96d36a98508b281317d2156182ae72c70fac Mon Sep 17 00:00:00 2001
From: Joe Guo <joeg at catalyst.net.nz>
Date: Thu, 1 Feb 2018 17:02:26 +1300
Subject: [PATCH 05/12] samba-tools/computer: replace filter flag for computer

UF_NORMAL_ACCOUNT to UF_WORKSTATION_TRUST_ACCOUNT

Signed-off-by: Joe Guo  <joeg at catalyst.net.nz>
---
 python/samba/netcmd/computer.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/python/samba/netcmd/computer.py b/python/samba/netcmd/computer.py
index 91f2e89..15041a3 100644
--- a/python/samba/netcmd/computer.py
+++ b/python/samba/netcmd/computer.py
@@ -484,7 +484,7 @@ class cmd_computer_list(Command):
         domain_dn = samdb.domain_dn()
         res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
                     expression=("(&(objectClass=computer)(userAccountControl:%s:=%u))"
-                    % (ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT)),
+                    % (ldb.OID_COMPARATOR_AND, dsdb.UF_WORKSTATION_TRUST_ACCOUNT)),
                     attrs=["samaccountname"])
         if (len(res) == 0):
             return
@@ -1748,7 +1748,7 @@ samba-tool computer syncpasswords --terminate \\
             dirsync_filter = "(&" + \
                                "(objectClass=computer)" + \
                                "(userAccountControl:%s:=%u)" % (
-                                ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \
+                                ldb.OID_COMPARATOR_AND, dsdb.UF_WORKSTATION_TRUST_ACCOUNT) + \
                                "(!(sAMAccountName=krbtgt*))" + \
                              ")"
 
-- 
2.7.4


From 5f5ccc5019c2500b7a68a5d14a32cba8ca3414c7 Mon Sep 17 00:00:00 2001
From: Joe Guo <joeg at catalyst.net.nz>
Date: Thu, 1 Feb 2018 13:34:08 +1300
Subject: [PATCH 06/12] samba-tools/computer: rm cmd_computer_add

This command is deprecated in user command, so delete here.

Signed-off-by: Joe Guo  <joeg at catalyst.net.nz>
---
 python/samba/netcmd/computer.py | 14 --------------
 1 file changed, 14 deletions(-)

diff --git a/python/samba/netcmd/computer.py b/python/samba/netcmd/computer.py
index 15041a3..9505250 100644
--- a/python/samba/netcmd/computer.py
+++ b/python/samba/netcmd/computer.py
@@ -384,19 +384,6 @@ Example5 shows how to create an RFC2307/NIS domain enabled computer account. If
         self.outf.write("Computer '%s' created successfully\n" % computername)
 
 
-class cmd_computer_add(cmd_computer_create):
-    __doc__ = cmd_computer_create.__doc__
-    # take this print out after the add subcommand is removed.
-    # the add subcommand is deprecated but left in for now to allow people to
-    # migrate to create
-
-    def run(self, *args, **kwargs):
-        self.outf.write(
-            "Note: samba-tool computer add is deprecated.  "
-            "Please use samba-tool computer create for the same function.\n")
-        return super(cmd_computer_add, self).run(*args, **kwargs)
-
-
 class cmd_computer_delete(Command):
     """Delete a computer.
 
@@ -2423,7 +2410,6 @@ class cmd_computer(SuperCommand):
     """Computer management."""
 
     subcommands = {}
-    subcommands["add"] = cmd_computer_add()
     subcommands["create"] = cmd_computer_create()
     subcommands["delete"] = cmd_computer_delete()
     subcommands["disable"] = cmd_computer_disable()
-- 
2.7.4


From 87c933ea0529cf12d2e1da2f90abb27863c81d8e Mon Sep 17 00:00:00 2001
From: Joe Guo <joeg at catalyst.net.nz>
Date: Thu, 1 Feb 2018 11:37:40 +1300
Subject: [PATCH 07/12] samba-tools/computer: append $ to sAMAccountName format
 string for computer

Although computer is also a `user`, but the sAMAccountName must end with $.

Signed-off-by: Joe Guo  <joeg at catalyst.net.nz>
---
 python/samba/netcmd/computer.py | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/python/samba/netcmd/computer.py b/python/samba/netcmd/computer.py
index 9505250..91c89e3 100644
--- a/python/samba/netcmd/computer.py
+++ b/python/samba/netcmd/computer.py
@@ -426,7 +426,7 @@ Example2 shows how to delete a computer in the domain against the local server.
         samdb = SamDB(url=H, session_info=system_session(),
                       credentials=creds, lp=lp)
 
-        filter = ("(&(sAMAccountName=%s)(sAMAccountType=805306368))" %
+        filter = ("(&(sAMAccountName=%s$)(sAMAccountType=805306368))" %
                    computername)
 
         try:
@@ -535,7 +535,7 @@ Example3 shows how to enable a computer in the domain against a local LDAP serve
             raise CommandError("Either the computername or '--filter' must be specified!")
 
         if filter is None:
-            filter = "(&(objectClass=computer)(sAMAccountName=%s))" % (ldb.binary_encode(computername))
+            filter = "(&(objectClass=computer)(sAMAccountName=%s$))" % (ldb.binary_encode(computername))
 
         lp = sambaopts.get_loadparm()
         creds = credopts.get_credentials(lp, fallback_machine=True)
@@ -574,7 +574,7 @@ class cmd_computer_disable(Command):
             raise CommandError("Either the computername or '--filter' must be specified!")
 
         if filter is None:
-            filter = "(&(objectClass=computer)(sAMAccountName=%s))" % (ldb.binary_encode(computername))
+            filter = "(&(objectClass=computer)(sAMAccountName=%s$))" % (ldb.binary_encode(computername))
 
         lp = sambaopts.get_loadparm()
         creds = credopts.get_credentials(lp, fallback_machine=True)
@@ -640,7 +640,7 @@ Example4 shows how to set the account expiration so that it will never expire.
             raise CommandError("Either the computername or '--filter' must be specified!")
 
         if filter is None:
-            filter = "(&(objectClass=computer)(sAMAccountName=%s))" % (ldb.binary_encode(computername))
+            filter = "(&(objectClass=computer)(sAMAccountName=%s$))" % (ldb.binary_encode(computername))
 
         lp = sambaopts.get_loadparm()
         creds = credopts.get_credentials(lp)
@@ -801,7 +801,7 @@ Example3 shows how an administrator would reset TestComputer3 computer's passwor
                 self.outf.write("Sorry, passwords do not match.\n")
 
         if filter is None:
-            filter = "(&(objectClass=computer)(sAMAccountName=%s))" % (ldb.binary_encode(computername))
+            filter = "(&(objectClass=computer)(sAMAccountName=%s$))" % (ldb.binary_encode(computername))
 
         lp = sambaopts.get_loadparm()
         creds = credopts.get_credentials(lp)
@@ -1428,7 +1428,7 @@ samba-tool computer getpassword --filter=samaccountname=TestComputer3 --attribut
             raise CommandError("Either the computername or '--filter' must be specified!")
 
         if filter is None:
-            filter = "(&(objectClass=computer)(sAMAccountName=%s))" % (ldb.binary_encode(computername))
+            filter = "(&(objectClass=computer)(sAMAccountName=%s$))" % (ldb.binary_encode(computername))
 
         if attributes is None:
             raise CommandError("Please specify --attributes")
@@ -2332,7 +2332,7 @@ LDAP server using the 'nano' editor.
         samdb = SamDB(url=H, session_info=system_session(),
                       credentials=creds, lp=lp)
 
-        filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
+        filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s$))" %
                   (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(computername)))
 
         domaindn = samdb.domain_dn()
-- 
2.7.4


From 197f56ecc1237c22c76b493d5e4df89789fb6bb6 Mon Sep 17 00:00:00 2001
From: Joe Guo <joeg at catalyst.net.nz>
Date: Fri, 2 Feb 2018 10:28:51 +1300
Subject: [PATCH 08/12] samba-tools/computer: replace sAMAccountType in
 cmd_computer_delete

Replace ATYPE_NORMAL_ACCOUNT to ATYPE_WORKSTATION_TRUST while filter for
object to delete.

Signed-off-by: Joe Guo  <joeg at catalyst.net.nz>
---
 python/samba/netcmd/computer.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/python/samba/netcmd/computer.py b/python/samba/netcmd/computer.py
index 91c89e3..d6c53ae 100644
--- a/python/samba/netcmd/computer.py
+++ b/python/samba/netcmd/computer.py
@@ -426,8 +426,8 @@ Example2 shows how to delete a computer in the domain against the local server.
         samdb = SamDB(url=H, session_info=system_session(),
                       credentials=creds, lp=lp)
 
-        filter = ("(&(sAMAccountName=%s$)(sAMAccountType=805306368))" %
-                   computername)
+        filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s$))" %
+                  (dsdb.ATYPE_WORKSTATION_TRUST, ldb.binary_encode(computername)))
 
         try:
             res = samdb.search(base=samdb.domain_dn(),
@@ -2333,7 +2333,7 @@ LDAP server using the 'nano' editor.
                       credentials=creds, lp=lp)
 
         filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s$))" %
-                  (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(computername)))
+                  (dsdb.ATYPE_WORKSTATION_TRUST, ldb.binary_encode(computername)))
 
         domaindn = samdb.domain_dn()
 
-- 
2.7.4


From fe4f3985ea40c0037ae89c9fd9218a6deb21f840 Mon Sep 17 00:00:00 2001
From: Joe Guo <joeg at catalyst.net.nz>
Date: Thu, 1 Feb 2018 11:56:09 +1300
Subject: [PATCH 09/12] samba-tools/computer: modify cmd_computer_create

Remove unsed options from user command, and adjust it for computer.

Signed-off-by: Joe Guo  <joeg at catalyst.net.nz>
---
 python/samba/netcmd/computer.py | 106 +++-------------------------------------
 1 file changed, 8 insertions(+), 98 deletions(-)

diff --git a/python/samba/netcmd/computer.py b/python/samba/netcmd/computer.py
index d6c53ae..57c5022 100644
--- a/python/samba/netcmd/computer.py
+++ b/python/samba/netcmd/computer.py
@@ -256,41 +256,12 @@ Example5 shows how to create an RFC2307/NIS domain enabled computer account. If
         Option("--random-password",
                 help="Generate random password",
                 action="store_true"),
-        Option("--smartcard-required",
-                help="Require a smartcard for interactive logons",
-                action="store_true"),
-        Option("--use-computername-as-cn",
-                help="Force use of computername as computer's CN",
-                action="store_true"),
         Option("--computerou",
                 help="DN of alternative location (without domainDN counterpart) to default CN=Computers in which new computer object will be created. E. g. 'OU=<OU name>'",
                 type=str),
-        Option("--surname", help="Computer's surname", type=str),
-        Option("--given-name", help="Computer's given name", type=str),
-        Option("--initials", help="Computer's initials", type=str),
-        Option("--profile-path", help="Computer's profile path", type=str),
-        Option("--script-path", help="Computer's logon script path", type=str),
-        Option("--home-drive", help="Computer's home drive letter", type=str),
-        Option("--home-directory", help="Computer's home directory path", type=str),
-        Option("--job-title", help="Computer's job title", type=str),
-        Option("--department", help="Computer's department", type=str),
-        Option("--company", help="Computer's company", type=str),
-        Option("--description", help="Computer's description", type=str),
-        Option("--mail-address", help="Computer's email address", type=str),
-        Option("--internet-address", help="Computer's home page", type=str),
-        Option("--telephone-number", help="Computer's phone number", type=str),
-        Option("--physical-delivery-office", help="Computer's office location", type=str),
-        Option("--rfc2307-from-nss",
-                help="Copy Unix computer attributes from NSS (will be overridden by explicit UID/GID/GECOS/shell)",
-                action="store_true"),
-        Option("--nis-domain", help="Computer's Unix/RFC2307 NIS domain", type=str),
-        Option("--unix-home", help="Computer's Unix/RFC2307 home directory",
-                type=str),
-        Option("--uid", help="Computer's Unix/RFC2307 computername", type=str),
-        Option("--uid-number", help="Computer's Unix/RFC2307 numeric UID", type=int),
-        Option("--gid-number", help="Computer's Unix/RFC2307 primary GID number", type=int),
-        Option("--gecos", help="Computer's Unix/RFC2307 GECOS field", type=str),
-        Option("--login-shell", help="Computer's Unix/RFC2307 login shell", type=str),
+        Option("--service-principal-name",
+                action='append',
+                help="Computer's Service Principal Name, can be provided multiple times"),
     ]
 
     takes_args = ["computername", "password?"]
@@ -303,81 +274,20 @@ Example5 shows how to create an RFC2307/NIS domain enabled computer account. If
 
     def run(self, computername, password=None, credopts=None, sambaopts=None,
             versionopts=None, H=None, must_change_at_next_login=False,
-            random_password=False, use_computername_as_cn=False, computerou=None,
-            surname=None, given_name=None, initials=None, profile_path=None,
-            script_path=None, home_drive=None, home_directory=None,
-            job_title=None, department=None, company=None, description=None,
-            mail_address=None, internet_address=None, telephone_number=None,
-            physical_delivery_office=None, rfc2307_from_nss=False,
-            nis_domain=None, unix_home=None, uid=None, uid_number=None,
-            gid_number=None, gecos=None, login_shell=None,
-            smartcard_required=False):
-
-        if smartcard_required:
-            if password is not None and password is not '':
-                raise CommandError('It is not allowed to specify '
-                                   '--newpassword '
-                                   'together with --smartcard-required.')
-            if must_change_at_next_login:
-                raise CommandError('It is not allowed to specify '
-                                   '--must-change-at-next-login '
-                                   'together with --smartcard-required.')
+            random_password=False, computerou=None,
+            service_principal_name=None):
 
-        if random_password and not smartcard_required:
+        if random_password:
             password = generate_random_password(128, 255)
 
-        while True:
-            if smartcard_required:
-                break
-            if password is not None and password is not '':
-                break
-            password = getpass("New Password: ")
-            passwordverify = getpass("Retype Password: ")
-            if not password == passwordverify:
-                password = None
-                self.outf.write("Sorry, passwords do not match.\n")
-
-        if rfc2307_from_nss:
-                pwent = pwd.getpwnam(computername)
-                if uid is None:
-                    uid = computername
-                if uid_number is None:
-                    uid_number = pwent[2]
-                if gid_number is None:
-                    gid_number = pwent[3]
-                if gecos is None:
-                    gecos = pwent[4]
-                if login_shell is None:
-                    login_shell = pwent[6]
-
         lp = sambaopts.get_loadparm()
         creds = credopts.get_credentials(lp)
 
-        if uid_number or gid_number:
-            if not lp.get("idmap_ldb:use rfc2307"):
-                self.outf.write("You are setting a Unix/RFC2307 UID or GID. You may want to set 'idmap_ldb:use rfc2307 = Yes' to use those attributes for XID/SID-mapping.\n")
-
-        if nis_domain is not None:
-            if None in (uid_number, login_shell, unix_home, gid_number):
-                raise CommandError('Missing parameters. To enable NIS features, '
-                                   'the following options have to be given: '
-                                   '--nis-domain=, --uidNumber=, --login-shell='
-                                   ', --unix-home=, --gid-number= Operation '
-                                   'cancelled.')
-
         try:
             samdb = SamDB(url=H, session_info=system_session(),
                           credentials=creds, lp=lp)
-            samdb.newuser(computername, password, force_password_change_at_next_login_req=must_change_at_next_login,
-                          usecomputernameascn=use_computername_as_cn, computerou=computerou, surname=surname, givenname=given_name, initials=initials,
-                          profilepath=profile_path, homedrive=home_drive, scriptpath=script_path, homedirectory=home_directory,
-                          jobtitle=job_title, department=department, company=company, description=description,
-                          mailaddress=mail_address, internetaddress=internet_address,
-                          telephonenumber=telephone_number, physicaldeliveryoffice=physical_delivery_office,
-                          nisdomain=nis_domain, unixhome=unix_home, uid=uid,
-                          uidnumber=uid_number, gidnumber=gid_number,
-                          gecos=gecos, loginshell=login_shell,
-                          smartcard_required=smartcard_required)
+            samdb.newcomputer(computername, password, force_password_change_at_next_login_req=must_change_at_next_login,
+                              computerou=computerou, serviceprincipalname=service_principal_name)
         except Exception, e:
             raise CommandError("Failed to add computer '%s': " % computername, e)
 
-- 
2.7.4


From 02cb4c6671b8f5449b475c45ce3fe8ca1190c288 Mon Sep 17 00:00:00 2001
From: Joe Guo <joeg at catalyst.net.nz>
Date: Fri, 2 Feb 2018 12:12:42 +1300
Subject: [PATCH 10/12] samba-tools/computer: update help docs

Modify commands doc string, which will be printed as help message.

Signed-off-by: Joe Guo  <joeg at catalyst.net.nz>
---
 python/samba/netcmd/computer.py | 73 +++++++++++++++--------------------------
 1 file changed, 27 insertions(+), 46 deletions(-)

diff --git a/python/samba/netcmd/computer.py b/python/samba/netcmd/computer.py
index 57c5022..17911ae 100644
--- a/python/samba/netcmd/computer.py
+++ b/python/samba/netcmd/computer.py
@@ -1,7 +1,7 @@
 # computer management
 #
-# Copyright Jelmer Vernooij 2010 <jelmer at samba.org>
-# Copyright Theresa Halloran 2011 <theresahalloran at gmail.com>
+# Copyright Garming Sam 2018 <garming at catalyst.net.nz>
+# Copyright Joe Guo 2018 <joeg at catalyst.net.nz>
 #
 # 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
@@ -207,43 +207,24 @@ if len(disabled_virtual_attributes) != 0:
 class cmd_computer_create(Command):
     """Create a new computer.
 
-This command creates a new computer account in the Active Directory domain.  The computername specified on the command is the sAMaccountName.
-
-Computer accounts may represent physical entities, such as people or may be used as service accounts for applications.  Computer accounts are also referred to as security principals and are assigned a security identifier (SID).
-
-A computer account enables a computer to logon to a computer and domain with an identity that can be authenticated.  To maximize security, each computer should have their own unique computer account and password.  A computer's access to domain resources is based on permissions assigned to the computer account.
-
-Unix (RFC2307) attributes may be added to the computer account. Attributes taken from NSS are obtained on the local machine. Explicitly given values override values obtained from NSS. Configure 'idmap_ldb:use rfc2307 = Yes' to use these attributes for UID/GID mapping.
+This command creates a new computer account in the Active Directory domain.  The computername specified on the command is the sAMAccountName (without ending $).
 
 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 create Computer1 passw0rd --given-name=John --surname=Smith --must-change-at-next-login -H ldap://samba.samdom.example.com -Uadministrator%passw1rd
+samba-tool computer create Computer1 passw0rd --must-change-at-next-login -H ldap://samba.samdom.example.com -Uadministrator%passw1rd
 
 Example1 shows how to create a new computer in the domain against a remote LDAP server.  The -H parameter is used to specify the remote target server.  The -U option is used to pass the userid and password authorized to issue the command remotely.
 
 Example2:
-sudo samba-tool computer create Computer2 passw2rd --given-name=Jane --surname=Doe --must-change-at-next-login
+sudo samba-tool computer create Computer2 passw2rd --must-change-at-next-login
 
-Example2 shows how to create a new computer in the domain against the local server.   sudo is used so a computer may run the command as root.  In this example, after Computer2 is created, he/she will be forced to change their password when they logon.
+Example2 shows how to create a new computer in the domain against the local server.   sudo is used so a user may run the command as root.  In this example, after Computer2 is created, it will be forced to change its password when it logons.
 
 Example3:
 samba-tool computer create Computer3 passw3rd --computerou='OU=OrgUnit'
 
 Example3 shows how to create a new computer in the OrgUnit organizational unit.
-
-Example4:
-samba-tool computer create Computer4 passw4rd --rfc2307-from-nss --gecos 'some text'
-
-Example4 shows how to create a new computer with Unix UID, GID and login-shell set from the local NSS and GECOS set to 'some text'.
-
-Example5:
-samba-tool computer create Computer5 passw5rd --nis-domain=samdom --unix-home=/home/Computer5 \
-           --uid-number=10005 --login-shell=/bin/false --gid-number=10000
-
-Example5 shows how to create an RFC2307/NIS domain enabled computer account. If
---nis-domain is set, then the other four parameters are mandatory.
-
 """
     synopsis = "%prog <computername> [<password>] [options]"
 
@@ -297,21 +278,21 @@ Example5 shows how to create an RFC2307/NIS domain enabled computer account. If
 class cmd_computer_delete(Command):
     """Delete a computer.
 
-This command deletes a computer account from the Active Directory domain.  The computername specified on the command is the sAMAccountName.
+This command deletes a computer account from the Active Directory domain.  The computername specified on the command is the sAMAccountName (without ending $).
 
 Once the account is deleted, all permissions and memberships associated with that account are deleted.  If a new computer account is added with the same name as a previously deleted account name, the new computer does not have the previous permissions.  The new account computer will be assigned a new security identifier (SID) and permissions and memberships will have to be added.
 
 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 delete Computer1 -H ldap://samba.samdom.example.com --computername=administrator --password=passw1rd
+samba-tool computer delete Computer1 -H ldap://samba.samdom.example.com --username=administrator --password=passw1rd
 
-Example1 shows how to delete a computer in the domain against a remote LDAP server.  The -H parameter is used to specify the remote target server.  The --computername= and --password= options are used to pass the computername and password of a computer that exists on the remote server and is authorized to issue the command on that server.
+Example1 shows how to delete a computer in the domain against a remote LDAP server.  The -H parameter is used to specify the remote target server.  The --username= and --password= options are used to pass the username and password of a user that exists on the remote server and is authorized to issue the command on that server.
 
 Example2:
 sudo samba-tool computer delete Computer2
 
-Example2 shows how to delete a computer in the domain against the local server.   sudo is used so a computer may run the command as root.
+Example2 shows how to delete a computer in the domain against the local server.   sudo is used so a user may run the command as root.
 
 """
     synopsis = "%prog <computername> [options]"
@@ -356,7 +337,7 @@ Example2 shows how to delete a computer in the domain against the local server.
 
 
 class cmd_computer_list(Command):
-    """List all users."""
+    """List all computers."""
 
     synopsis = "%prog [options]"
 
@@ -391,7 +372,7 @@ class cmd_computer_list(Command):
 
 
 class cmd_computer_enable(Command):
-    """Enable an computer.
+    """Enable a computer.
 
 This command enables a computer account for logon to an Active Directory domain.  The computername specified on the command is the sAMAccountName.  The computername may also be specified using the --filter option.
 
@@ -407,17 +388,17 @@ Additionally, the enable function allows an administrator to have a set of creat
 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 enable Testuser1 --URL=ldap://samba.samdom.example.com --computername=administrator --password=passw1rd
+samba-tool computer enable Testcomputer1 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
 
-Example1 shows how to enable a computer in the domain against a remote LDAP server.  The --URL parameter is used to specify the remote target server.  The --computername= and --password= options are used to pass the computername and password of a computer that exists on the remote server and is authorized to update that server.
+Example1 shows how to enable a computer in the domain against a remote LDAP server.  The --URL parameter is used to specify the remote target server.  The --username= and --password= options are used to pass the computername and password of a computer that exists on the remote server and is authorized to update that server.
 
 Example2:
-su samba-tool computer enable Testuser2
+su samba-tool computer enable Testcomputer2
 
-Example2 shows how to enable computer Testuser2 for use in the domain on the local server. sudo is used so a computer may run the command as root.
+Example2 shows how to enable computer Testcomputer2 for use in the domain on the local server. sudo is used so a computer may run the command as root.
 
 Example3:
-samba-tool computer enable --filter=samaccountname=Testuser3
+samba-tool computer enable --filter=samaccountname=Testcomputer3$
 
 Example3 shows how to enable a computer in the domain against a local LDAP server.  It uses the --filter=samaccountname to specify the computername.
 
@@ -460,7 +441,7 @@ Example3 shows how to enable a computer in the domain against a local LDAP serve
 
 
 class cmd_computer_disable(Command):
-    """Disable an computer."""
+    """Disable a computer."""
 
     synopsis = "%prog (<computername>|--filter <filter>) [options]"
 
@@ -507,23 +488,23 @@ When a computer account expires, it becomes disabled and the computer is unable
 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 on a remote server.
 
 Example1:
-samba-tool computer setexpiry Computer1 --days=20 --URL=ldap://samba.samdom.example.com --computername=administrator --password=passw1rd
+samba-tool computer setexpiry Computer1 --days=20 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
 
-Example1 shows how to set the expiration of an account in a remote LDAP server.  The --URL parameter is used to specify the remote target server.  The --computername= and --password= options are used to pass the computername and password of a computer that exists on the remote server and is authorized to update that server.
+Example1 shows how to set the expiration of an account in a remote LDAP server.  The --URL parameter is used to specify the remote target server.  The --username= and --password= options are used to pass the username and password of a user that exists on the remote server and is authorized to update that server.
 
 Example2:
 su samba-tool computer setexpiry Computer2
 
-Example2 shows how to set the account expiration of computer Computer2 so it will never expire.  The computer in this example resides on the  local server.   sudo is used so a computer may run the command as root.
+Example2 shows how to set the account expiration of computer Computer2 so it will never expire.  The computer in this example resides on the  local server.   sudo is used so a computer may run the user as root.
 
 Example3:
-samba-tool computer setexpiry --days=20 --filter=samaccountname=Computer3
+samba-tool computer setexpiry --days=20 --filter=samaccountname=Computer3$
 
 Example3 shows how to set the account expiration date to end of day 20 days from the current day.  The computername or sAMAccountName is specified using the --filter= parameter and the computername in this example is Computer3.
 
 Example4:
 samba-tool computer setexpiry --noexpiry Computer4
-Example4 shows how to set the account expiration so that it will never expire.  The computername and sAMAccountName in this example is Computer4.
+Example4 shows how to set the account expiration so that it will never expire.  The computername in this example is Computer4, and sAMAccountName will be Computer4$.
 
 """
     synopsis = "%prog (<computername>|--filter <filter>) [options]"
@@ -620,7 +601,7 @@ class cmd_computer_password(Command):
 class cmd_computer_setpassword(Command):
     """Set or reset the password of a computer account.
 
-This command sets or resets the logon password for a computer account.  The computername specified on the command is the sAMAccountName.  The computername may also be specified using the --filter option.
+This command sets or resets the logon password for a computer account.  The computername specified on the command is the sAMAccountName without ending $.  The computername may also be specified using the --filter option.
 
 If the password is not specified on the command through the --newpassword parameter, the computer is prompted for the password to be entered through the command line.
 
@@ -631,7 +612,7 @@ The command may be run from the root userid or another authorized userid.  The -
 Example1:
 samba-tool computer setpassword TestComputer1 --newpassword=passw0rd --URL=ldap://samba.samdom.example.com -Uadministrator%passw1rd
 
-Example1 shows how to set the password of computer TestComputer1 on a remote LDAP server.  The --URL parameter is used to specify the remote target server.  The -U option is used to pass the computername and password of a computer that exists on the remote server and is authorized to update the server.
+Example1 shows how to set the password of computer TestComputer1 on a remote LDAP server.  The --URL parameter is used to specify the remote target server.  The -U option is used to pass the username and password of a computer that exists on the remote server and is authorized to update the server.
 
 Example2:
 sudo samba-tool computer setpassword TestComputer2 --newpassword=passw0rd --must-change-at-next-login
@@ -639,9 +620,9 @@ sudo samba-tool computer setpassword TestComputer2 --newpassword=passw0rd --must
 Example2 shows how an administrator would reset the TestComputer2 computer's password to passw0rd.  The computer is running under the root userid using the sudo command.  In this example the computer TestComputer2 must change their password the next time they logon to the account.
 
 Example3:
-samba-tool computer setpassword --filter=samaccountname=TestComputer3 --newpassword=passw0rd
+samba-tool computer setpassword --filter=samaccountname=TestComputer3$ --newpassword=passw0rd
 
-Example3 shows how an administrator would reset TestComputer3 computer's password to passw0rd using the --filter= option to specify the computername.
+Example3 shows how an administrator would reset TestComputer3 computer's password to passw0rd using the --filter= option to specify the sAMAccountName (must ends with $).
 
 """
     synopsis = "%prog (<computername>|--filter <filter>) [options]"
-- 
2.7.4


From b24509de002a2c76adb18afd9ffba0900b2ab46a Mon Sep 17 00:00:00 2001
From: Joe Guo <joeg at catalyst.net.nz>
Date: Fri, 2 Feb 2018 18:01:59 +1300
Subject: [PATCH 11/12] samba-tools/computer: create and remove DNS records for
 computer

While creating a computer, user can provide IPv4 and/or IPv6,
then the command will create A or AAAA record for computer,
and set owner infomation.

While deleting a computer, the command will cleanup DNS records for the
computer.

Also improve code style for pep8.

Signed-off-by: Joe Guo  <joeg at catalyst.net.nz>
---
 python/samba/netcmd/computer.py | 247 ++++++++++++++++++++++++++++++++++++----
 python/samba/samdb.py           |  16 ++-
 2 files changed, 238 insertions(+), 25 deletions(-)

diff --git a/python/samba/netcmd/computer.py b/python/samba/netcmd/computer.py
index 17911ae..2cfc3ca 100644
--- a/python/samba/netcmd/computer.py
+++ b/python/samba/netcmd/computer.py
@@ -17,7 +17,6 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 
-import samba.getopt as options
 import ldb
 import pwd
 import os
@@ -31,8 +30,13 @@ import errno
 import time
 import base64
 import binascii
+import socket
 from subprocess import Popen, PIPE, STDOUT, check_call, CalledProcessError
 from getpass import getpass
+import samba
+import samba.getopt as options
+from samba.dnsserver import ARecord, AAAARecord
+from samba.dcerpc import dnsserver, dnsp
 from samba.auth import system_session
 from samba.samdb import SamDB
 from samba.dcerpc import misc
@@ -45,7 +49,11 @@ from samba import (
     gensec,
     generate_random_password,
     Ldb,
+    sd_utils,
+    werror,
+    WERRORError
     )
+from samba.remove_dc import remove_dns_references
 from samba.net import Net
 
 from samba.netcmd import (
@@ -243,6 +251,10 @@ Example3 shows how to create a new computer in the OrgUnit organizational unit.
         Option("--service-principal-name",
                 action='append',
                 help="Computer's Service Principal Name, can be provided multiple times"),
+        Option("--ip-address",
+                action='append',
+                default=[],
+                help="IPv4 address for the computer's A record, or IPv6 address for AAAA record, can be provided multiple times"),
     ]
 
     takes_args = ["computername", "password?"]
@@ -251,12 +263,26 @@ Example3 shows how to create a new computer in the OrgUnit organizational unit.
         "sambaopts": options.SambaOptions,
         "credopts": options.CredentialsOptions,
         "versionopts": options.VersionOptions,
-        }
+    }
 
-    def run(self, computername, password=None, credopts=None, sambaopts=None,
-            versionopts=None, H=None, must_change_at_next_login=False,
-            random_password=False, computerou=None,
-            service_principal_name=None):
+    def run(
+            self,
+            computername, password=None,
+            credopts=None, sambaopts=None, versionopts=None,
+            H=None,
+            must_change_at_next_login=False,
+            random_password=False,
+            computerou=None,
+            service_principal_name=None,
+            ip_address=None,
+        ):
+
+        # check each IP address if provided
+        for ip in ip_address:
+            try:
+                socket.inet_aton(ip)
+            except socket.error:
+                raise CommandError('Invalid IP address {}'.format(ip))
 
         if random_password:
             password = generate_random_password(128, 255)
@@ -265,16 +291,172 @@ Example3 shows how to create a new computer in the OrgUnit organizational unit.
         creds = credopts.get_credentials(lp)
 
         try:
-            samdb = SamDB(url=H, session_info=system_session(),
-                          credentials=creds, lp=lp)
-            samdb.newcomputer(computername, password, force_password_change_at_next_login_req=must_change_at_next_login,
-                              computerou=computerou, serviceprincipalname=service_principal_name)
+            samdb = SamDB(
+                url=H,
+                session_info=system_session(),
+                credentials=creds,
+                lp=lp,
+            )
+
+            samdb.newcomputer(
+                computername, password,
+                force_password_change_at_next_login_req=must_change_at_next_login,
+                computerou=computerou,
+                serviceprincipalname=service_principal_name,
+                ipaddresses=ip_address,
+            )
+
+            filter = '(&(sAMAccountName={}$)(objectclass=computer))'.format(
+                ldb.binary_encode(computername),
+            )
+
+            recs = samdb.search(
+                base=samdb.domain_dn(),
+                scope=ldb.SCOPE_SUBTREE,
+                expression=filter,
+                attrs=['primaryGroupID', 'objectSid'],
+            )
+
+            group = recs[0]['primaryGroupID'][0]
+            owner = ndr_unpack(security.dom_sid, recs[0]["objectSid"][0])
+
+            dns_conn = dnsserver.dnsserver(
+                "ncacn_ip_tcp:{}[sign]".format(samdb.host_dns_name()),
+                lp,
+                creds,
+            )
+
+            change_owner_sd = security.descriptor()
+            change_owner_sd.owner_sid = owner
+            change_owner_sd.group_sid = security.dom_sid(
+                "{}-{}".format(samdb.get_domain_sid(), group),
+            )
+
+            join_add_dns_records(
+                samdb, computername, dns_conn, change_owner_sd,
+                samdb.host_dns_name(), ip_address, self.get_logger(),
+            )
+
         except Exception, e:
             raise CommandError("Failed to add computer '%s': " % computername, e)
 
         self.outf.write("Computer '%s' created successfully\n" % computername)
 
 
+def join_add_dns_records(
+        samdb, name, dns_conn, change_owner_sd,
+        server, IPs, logger,
+    ):
+    """Remotely Add a DNS record to the target DC.  We assume that if we
+       replicate DNS that the server holds the DNS roles and can accept
+       updates.
+
+       This avoids issues getting replication going after the DC
+       first starts as the rest of the domain does not have to
+       wait for samba_dnsupdate to run successfully.
+
+       Specifically, we add the records implied by the DsReplicaUpdateRefs
+       call above.
+
+       We do not just run samba_dnsupdate as we want to strictly
+       operate against the DC we just joined:
+        - We do not want to query another DNS server
+        - We do not want to obtain a Kerberos ticket
+          (as the KDC we select may not be the DC we just joined,
+          and so may not be in sync with the password we just set)
+        - We do not wish to set the _ldap records until we have started
+        - We do not wish to use NTLM (the --use-samba-tool mode forces
+          NTLM)
+
+    """
+
+    client_version = dnsserver.DNS_CLIENT_VERSION_LONGHORN
+    record_type = dnsp.DNS_TYPE_A
+    select_flags = dnsserver.DNS_RPC_VIEW_AUTHORITY_DATA | dnsserver.DNS_RPC_VIEW_NO_CHILDREN
+    zone = samdb.domain_dns_name()
+    name_found = True
+    sd_helper = samba.sd_utils.SDUtils(samdb)
+
+    try:
+        buflen, res = dns_conn.DnssrvEnumRecords2(
+            client_version,
+            0,
+            server,
+            zone,
+            name,
+            None,
+            dnsp.DNS_TYPE_ALL,
+            select_flags,
+            None,
+            None,
+        )
+    except WERRORError as e:
+        if e.args[0] == werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST:
+            name_found = False
+            pass
+
+    if name_found:
+        for rec in res.rec:
+            for record in rec.records:
+                if record.wType == dnsp.DNS_TYPE_A or record.wType == dnsp.DNS_TYPE_AAAA:
+                    # delete record
+                    del_rec_buf = dnsserver.DNS_RPC_RECORD_BUF()
+                    del_rec_buf.rec = record
+                    try:
+                        dns_conn.DnssrvUpdateRecord2(
+                            client_version,
+                            0,
+                            server,
+                            zone,
+                            name,
+                            None,
+                            del_rec_buf,
+                        )
+                    except WERRORError as e:
+                        if e.args[0] != werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST:
+                            raise
+
+    for IP in IPs:
+        if IP.find(':') != -1:
+            logger.info("Adding DNS AAAA record %s.%s for IPv6 IP: %s" % (name, zone, IP))
+            rec = AAAARecord(IP)
+        else:
+            logger.info("Adding DNS A record %s.%s for IPv4 IP: %s" % (name, zone, IP))
+            rec = ARecord(IP)
+
+        # Add record
+        add_rec_buf = dnsserver.DNS_RPC_RECORD_BUF()
+        add_rec_buf.rec = rec
+
+        dns_conn.DnssrvUpdateRecord2(
+            client_version,
+            0,
+            server,
+            zone,
+            name,
+            add_rec_buf,
+            None,
+        )
+
+    if (len(IPs) > 0):
+        domaindns_zone_dn = ldb.Dn(
+            samdb,
+            'DC=DomainDnsZones,%s' % samdb.get_default_basedn(),
+        )
+
+        dns_a_dn, ldap_record = samdb.dns_lookup(
+            "%s.%s" % (name, zone),
+            dns_partition=domaindns_zone_dn,
+        )
+
+        # Make the DC own the DNS record, not the administrator
+        sd_helper.modify_sd_on_dn(
+            dns_a_dn,
+            change_owner_sd,
+            controls=["sd_flags:1:%d" % (security.SECINFO_OWNER | security.SECINFO_GROUP)],
+        )
+
+
 class cmd_computer_delete(Command):
     """Delete a computer.
 
@@ -307,30 +489,53 @@ Example2 shows how to delete a computer in the domain against the local server.
         "sambaopts": options.SambaOptions,
         "credopts": options.CredentialsOptions,
         "versionopts": options.VersionOptions,
-        }
+    }
+
+    def run(
+            self, computername,
+            credopts=None, sambaopts=None, versionopts=None,
+            H=None,
+        ):
 
-    def run(self, computername, credopts=None, sambaopts=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)
+        samdb = SamDB(
+            url=H,
+            session_info=system_session(),
+            credentials=creds,
+            lp=lp,
+        )
 
-        filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s$))" %
-                  (dsdb.ATYPE_WORKSTATION_TRUST, ldb.binary_encode(computername)))
+        filter = "(&(sAMAccountType={})(sAMAccountName={}$))".format(
+            dsdb.ATYPE_WORKSTATION_TRUST,
+            ldb.binary_encode(computername),
+        )
 
+        dns_host_name = None
         try:
-            res = samdb.search(base=samdb.domain_dn(),
-                               scope=ldb.SCOPE_SUBTREE,
-                               expression=filter,
-                               attrs=["dn"])
+            res = samdb.search(
+                base=samdb.domain_dn(),
+                scope=ldb.SCOPE_SUBTREE,
+                expression=filter,
+                attrs=["dn", "dNSHostName"],
+            )
             computer_dn = res[0].dn
+            if "dNSHostName" in res[0]:
+                dns_host_name = res[0]["dNSHostName"][0]
+
         except IndexError:
             raise CommandError('Unable to find computer "%s"' % (computername))
 
         try:
             samdb.delete(computer_dn)
+            if dns_host_name is not None:
+                remove_dns_references(
+                    samdb,
+                    self.get_logger(),
+                    dns_host_name,
+                    ignore_no_name=True,
+                )
         except Exception, e:
             raise CommandError('Failed to remove computer "%s"' % computername, e)
         self.outf.write("Deleted computer %s\n" % computername)
diff --git a/python/samba/samdb.py b/python/samba/samdb.py
index e2cd3af..71351dc 100644
--- a/python/samba/samdb.py
+++ b/python/samba/samdb.py
@@ -301,7 +301,9 @@ member: %s
     def newcomputer(self, computername, password,
                     force_password_change_at_next_login_req=False,
                     computerou=None, setpassword=True,
-                    serviceprincipalname=None):
+                    serviceprincipalname=None,
+                    ipaddresses=None,
+        ):
         """Adds a new computer with additional parameters
 
         :param computername: Name of the new computer(without ending $)
@@ -318,11 +320,16 @@ member: %s
             'dn': "CN=%s,%s,%s" % (computername, computerou, self.domain_dn()),
             'sAMAccountName': sAMAccountName,
             'userAccountControl': str(dsdb.UF_WORKSTATION_TRUST_ACCOUNT),
-            'dNSHostName': '{}.{}'.format(computername, self.domain_dns_name()),
             'servicePrincipalName': serviceprincipalname,
             'objectClass': 'computer',
         }
 
+        if ipaddresses:
+            ldbmessage['dNSHostName'] = '{}.{}'.format(
+                computername,
+                self.domain_dns_name(),
+            )
+
         self.transaction_start()
         try:
             self.add(ldbmessage)
@@ -330,9 +337,10 @@ member: %s
             # Sets the password for it
             if setpassword:
                 self.setpassword(
-                    "(sAMAccountName=%s)" % sAMAccountName, # alreay ends with $
+                    "(sAMAccountName={})".format(sAMAccountName), # alreay ends with $
                     password,
-                    force_password_change_at_next_login_req)
+                    force_password_change_at_next_login_req,
+                )
 
         except:
             self.transaction_cancel()
-- 
2.7.4


From 746af272e229900169edb8560e53b0467e6fe586 Mon Sep 17 00:00:00 2001
From: Joe Guo <joeg at catalyst.net.nz>
Date: Mon, 5 Feb 2018 14:50:10 +1300
Subject: [PATCH 12/12] samba-tools/computer: add tests for samba-tools
 computer command

Copy the tests for user command, and make changes.

Signed-off-by: Joe Guo  <joeg at catalyst.net.nz>
---
 python/samba/netcmd/computer.py           |  67 ++---
 python/samba/tests/samba_tool/computer.py | 402 ++++++++++++++++++++++++++++++
 source4/selftest/tests.py                 |   1 +
 3 files changed, 418 insertions(+), 52 deletions(-)
 create mode 100644 python/samba/tests/samba_tool/computer.py

diff --git a/python/samba/netcmd/computer.py b/python/samba/netcmd/computer.py
index 2cfc3ca..2c4c4bb 100644
--- a/python/samba/netcmd/computer.py
+++ b/python/samba/netcmd/computer.py
@@ -849,45 +849,22 @@ Example3 shows how an administrator would reset TestComputer3 computer's passwor
         Option("--random-password",
                 help="Generate random password",
                 action="store_true"),
-        Option("--smartcard-required",
-                help="Require a smartcard for interactive logons",
-                action="store_true"),
-        Option("--clear-smartcard-required",
-                help="Don't require a smartcard for interactive logons",
-                action="store_true"),
         ]
 
     takes_args = ["computername?"]
 
     def run(self, computername=None, filter=None, credopts=None, sambaopts=None,
             versionopts=None, H=None, newpassword=None,
-            must_change_at_next_login=False, random_password=False,
-            smartcard_required=False, clear_smartcard_required=False):
+            must_change_at_next_login=False, random_password=False):
         if filter is None and computername is None:
             raise CommandError("Either the computername or '--filter' must be specified!")
 
         password = newpassword
 
-        if smartcard_required:
-            if password is not None and password is not '':
-                raise CommandError('It is not allowed to specify '
-                                   '--newpassword '
-                                   'together with --smartcard-required.')
-            if must_change_at_next_login:
-                raise CommandError('It is not allowed to specify '
-                                   '--must-change-at-next-login '
-                                   'together with --smartcard-required.')
-            if clear_smartcard_required:
-                raise CommandError('It is not allowed to specify '
-                                   '--clear-smartcard-required '
-                                   'together with --smartcard-required.')
-
-        if random_password and not smartcard_required:
+        if random_password:
             password = generate_random_password(128, 255)
 
         while True:
-            if smartcard_required:
-                break
             if password is not None and password is not '':
                 break
             password = getpass("New Password: ")
@@ -907,33 +884,19 @@ Example3 shows how an administrator would reset TestComputer3 computer's passwor
         samdb = SamDB(url=H, session_info=system_session(),
                       credentials=creds, lp=lp)
 
-        if smartcard_required:
-            command = ""
-            try:
-                command = "Failed to set UF_SMARTCARD_REQUIRED for computer '%s'" % (computername or filter)
-                flags = dsdb.UF_SMARTCARD_REQUIRED
-                samdb.toggle_userAccountFlags(filter, flags, on=True)
-                command = "Failed to enable account for computer '%s'" % (computername or filter)
-                samdb.enable_account(filter)
-            except Exception, msg:
-                # FIXME: catch more specific exception
-                raise CommandError("%s: %s" % (command, msg))
-            self.outf.write("Added UF_SMARTCARD_REQUIRED OK\n")
-        else:
-            command = ""
-            try:
-                if clear_smartcard_required:
-                    command = "Failed to remove UF_SMARTCARD_REQUIRED for computer '%s'" % (computername or filter)
-                    flags = dsdb.UF_SMARTCARD_REQUIRED
-                    samdb.toggle_userAccountFlags(filter, flags, on=False)
-                command = "Failed to set password for computer '%s'" % (computername or filter)
-                samdb.setpassword(filter, password,
-                                  force_change_at_next_login=must_change_at_next_login,
-                                  computername=computername)
-            except Exception, msg:
-                # FIXME: catch more specific exception
-                raise CommandError("%s: %s" % (command, msg))
-            self.outf.write("Changed password OK\n")
+        command = ""
+        try:
+            command = "Failed to set password for computer '%s'" % (computername or filter)
+            samdb.setpassword(
+                filter, password,
+                force_change_at_next_login=must_change_at_next_login,
+                # for computer, username is computername + '$'
+                username=computername+'$',
+            )
+        except Exception, msg:
+            # FIXME: catch more specific exception
+            raise CommandError("%s: %s" % (command, msg))
+        self.outf.write("Changed password OK\n")
 
 class GetPasswordCommand(Command):
 
diff --git a/python/samba/tests/samba_tool/computer.py b/python/samba/tests/samba_tool/computer.py
new file mode 100644
index 0000000..b40af57
--- /dev/null
+++ b/python/samba/tests/samba_tool/computer.py
@@ -0,0 +1,402 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Sean Dague <sdague at linux.vnet.ibm.com> 2011
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+import os
+import time
+import base64
+import ldb
+from samba.tests.samba_tool.base import SambaToolCmdTest
+from samba import credentials, nttime2unix, dsdb
+from samba.ndr import ndr_unpack
+from samba.dcerpc import drsblobs
+
+
+class ComputerCmdTestCase(SambaToolCmdTest):
+    """Tests for samba-tool computer subcommands"""
+    computers = []
+    samdb = None
+
+    def setUp(self):
+        super(ComputerCmdTestCase, self).setUp()
+
+        self.samdb = self.getSamDB(
+            "-H", "ldap://%s" % os.environ["DC_SERVER"],
+            "-U{}%{}".format(os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"])
+        )
+
+        self.computers = [
+            self._randomComputer({"name": "sambatool1", "service_principal_name": "xxx1"}),
+            self._randomComputer({"name": "sambatool2", "service_principal_name": "xxx1"}),
+            self._randomComputer({"name": "sambatool3", "service_principal_name": "xxx2"}),
+            self._randomComputer({"name": "sambatool4", "service_principal_name": "xxx2"}),
+        ]
+
+        # setup the 8 computers and ensure they are correct
+        for computer in self.computers:
+            (result, out, err) = computer["createComputerFn"](computer)
+            self.assertCmdSuccess(result, out, err)
+            self.assertEquals(err,"","Shouldn't be any error messages")
+            self.assertIn("Computer '%s' created successfully" % computer["name"], out)
+
+            computer["checkComputerFn"](computer)
+
+
+    def tearDown(self):
+        super(ComputerCmdTestCase, self).tearDown()
+        # clean up all the left over computers, just in case
+        for computer in self.computers:
+            if self._find_computer(computer["name"]):
+                self.runsubcmd("computer", "delete", computer["name"])
+
+
+    def test_newcomputer(self):
+        # try to add all the computers again, this should fail
+        for computer in self.computers:
+            (result, out, err) = self._create_computer(computer)
+            self.assertCmdFail(result, "Ensure that create computer fails")
+            self.assertIn("LDAP error 68 LDAP_ENTRY_ALREADY_EXISTS", err)
+
+        # try to delete all the 4 computers we just added
+        for computer in self.computers:
+            (result, out, err) = self.runsubcmd("computer", "delete", computer["name"])
+            self.assertCmdSuccess(result, out, err, "Can we delete computers")
+            found = self._find_computer(computer["name"])
+            self.assertIsNone(found)
+
+        # test adding computers 
+        for computer in self.computers:
+            (result, out, err) =  self.runsubcmd(
+                "computer", "create", 
+                computer["name"], computer["password"],
+                "--service-principal-name", computer["service_principal_name"],
+                "-H", "ldap://%s" % os.environ["DC_SERVER"],
+                "-U{}%{}".format(os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"]),
+            )
+
+            self.assertCmdSuccess(result, out, err)
+            self.assertEquals(err,"","Shouldn't be any error messages")
+            self.assertIn("Computer '%s' created successfully" % computer["name"], out)
+
+            found = self._find_computer(computer["name"])
+
+            self.assertEquals("%s" % found.get("cn"), "%(name)s" % computer)
+            self.assertEquals("%s" % found.get("name"), "%(name)s" % computer)
+
+    def _verify_supplementalCredentials(self, ldif,
+                                        min_packages=3,
+                                        max_packages=6):
+        msgs = self.samdb.parse_ldif(ldif)
+        (changetype, obj) = next(msgs)
+
+        self.assertIn("supplementalCredentials", obj, "supplementalCredentials attribute required")
+        sc_blob = obj["supplementalCredentials"][0]
+        sc = ndr_unpack(drsblobs.supplementalCredentialsBlob, sc_blob)
+
+        self.assertGreaterEqual(sc.sub.num_packages,
+                                min_packages, "min_packages check")
+        self.assertLessEqual(sc.sub.num_packages,
+                             max_packages, "max_packages check")
+
+        if max_packages == 0:
+            return
+
+        def find_package(packages, name, start_idx=0):
+            for i in xrange(start_idx, len(packages)):
+                if packages[i].name == name:
+                    return (i, packages[i])
+            return (None, None)
+
+        # The ordering is this
+        #
+        # Primary:Kerberos-Newer-Keys (optional)
+        # Primary:Kerberos
+        # Primary:WDigest
+        # Primary:CLEARTEXT (optional)
+        # Primary:SambaGPG (optional)
+        #
+        # And the 'Packages' package is insert before the last
+        # other package.
+
+        nidx = 0
+        (pidx, pp) = find_package(sc.sub.packages, "Packages", start_idx=nidx)
+        self.assertIsNotNone(pp, "Packages required")
+        self.assertEqual(pidx + 1, sc.sub.num_packages - 1,
+                         "Packages needs to be at num_packages - 1")
+
+        (knidx, knp) = find_package(sc.sub.packages, "Primary:Kerberos-Newer-Keys",
+                                    start_idx=nidx)
+        if knidx is not None:
+            self.assertEqual(knidx, nidx, "Primary:Kerberos-Newer-Keys at wrong position")
+            nidx = nidx + 1
+            if nidx == pidx:
+                nidx = nidx + 1
+
+        (kidx, kp) = find_package(sc.sub.packages, "Primary:Kerberos",
+                                    start_idx=nidx)
+        self.assertIsNotNone(pp, "Primary:Kerberos required")
+        self.assertEqual(kidx, nidx, "Primary:Kerberos at wrong position")
+        nidx = nidx + 1
+        if nidx == pidx:
+            nidx = nidx + 1
+
+        (widx, wp) = find_package(sc.sub.packages, "Primary:WDigest",
+                                  start_idx=nidx)
+        self.assertIsNotNone(pp, "Primary:WDigest required")
+        self.assertEqual(widx, nidx, "Primary:WDigest at wrong position")
+        nidx = nidx + 1
+        if nidx == pidx:
+            nidx = nidx + 1
+
+        (cidx, cp) = find_package(sc.sub.packages, "Primary:CLEARTEXT",
+                                    start_idx=nidx)
+        if cidx is not None:
+            self.assertEqual(cidx, nidx, "Primary:CLEARTEXT at wrong position")
+            nidx = nidx + 1
+            if nidx == pidx:
+                nidx = nidx + 1
+
+        (gidx, gp) = find_package(sc.sub.packages, "Primary:SambaGPG",
+                                  start_idx=nidx)
+        if gidx is not None:
+            self.assertEqual(gidx, nidx, "Primary:SambaGPG at wrong position")
+            nidx = nidx + 1
+            if nidx == pidx:
+                nidx = nidx + 1
+
+        self.assertEqual(nidx, sc.sub.num_packages, "Unknown packages found")
+
+    def test_setpassword(self):
+        for computer in self.computers:
+            newpasswd = self.randomPass()
+            (result, out, err) = self.runsubcmd(
+                "computer", "setpassword", 
+                computer["name"],
+                "--newpassword=%s" % newpasswd,
+                "-H", "ldap://%s" % os.environ["DC_SERVER"],
+                "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"])
+            )
+            self.assertCmdSuccess(result, out, err, "Ensure setpassword runs")
+            self.assertEquals(err,"","setpassword with url")
+            self.assertMatch(out, "Changed password OK", "setpassword with url")
+
+        attributes = "sAMAccountName,unicodePwd,supplementalCredentials,virtualClearTextUTF8,virtualClearTextUTF16,virtualSSHA,virtualSambaGPG"
+        (result, out, err) = self.runsubcmd("computer", "syncpasswords",
+                                            "--cache-ldb-initialize",
+                                            "--attributes=%s" % attributes,
+                                            "--decrypt-samba-gpg")
+        self.assertCmdSuccess(result, out, err, "Ensure syncpasswords --cache-ldb-initialize runs")
+        self.assertEqual(err,"","getpassword without url")
+        cache_attrs = {
+            "objectClass": { "value": "userSyncPasswords" },
+            "samdbUrl": { },
+            "dirsyncFilter": { },
+            "dirsyncAttribute": { },
+            "dirsyncControl": { "value": "dirsync:1:0:0"},
+            "passwordAttribute": { },
+            "decryptSambaGPG": { },
+            "currentTime": { },
+        }
+        for a in cache_attrs.keys():
+            v = cache_attrs[a].get("value", "")
+            self.assertMatch(out, "%s: %s" % (a, v),
+                "syncpasswords --cache-ldb-initialize: %s: %s out[%s]" % (a, v, out))
+
+        (result, out, err) = self.runsubcmd("computer", "syncpasswords", "--no-wait")
+        self.assertCmdSuccess(result, out, err, "Ensure syncpasswords --no-wait runs")
+        self.assertEqual(err,"","syncpasswords --no-wait")
+        self.assertMatch(out, "dirsync_loop(): results 0",
+            "syncpasswords --no-wait: 'dirsync_loop(): results 0': out[%s]" % (out))
+        for computer in self.computers:
+            self.assertMatch(out, "sAMAccountName: %s$" % (computer["name"]),
+                "syncpasswords --no-wait: 'sAMAccountName': %s$ out[%s]" % (computer["name"], out))
+
+        for computer in self.computers:
+            newpasswd = self.randomPass()
+            creds = credentials.Credentials()
+            creds.set_anonymous()
+            creds.set_password(newpasswd)
+            nthash = creds.get_nt_hash()
+            unicodePwd = base64.b64encode(creds.get_nt_hash())
+            virtualClearTextUTF8 = base64.b64encode(newpasswd)
+            virtualClearTextUTF16 = base64.b64encode(unicode(newpasswd, 'utf-8').encode('utf-16-le'))
+
+            (result, out, err) = self.runsubcmd(
+                "computer", "setpassword", computer["name"],
+                "--newpassword=%s" % newpasswd)
+            self.assertCmdSuccess(result, out, err, "Ensure setpassword runs")
+            self.assertEquals(err,"","setpassword without url")
+            self.assertMatch(out, "Changed password OK", "setpassword without url")
+
+            (result, out, err) = self.runsubcmd("computer", "syncpasswords", "--no-wait")
+            self.assertCmdSuccess(result, out, err, "Ensure syncpasswords --no-wait runs")
+            self.assertEqual(err,"","syncpasswords --no-wait")
+            self.assertMatch(out, "dirsync_loop(): results 0",
+                "syncpasswords --no-wait: 'dirsync_loop(): results 0': out[%s]" % (out))
+            self.assertMatch(out, "sAMAccountName: %s$" % (computer["name"]),
+                "syncpasswords --no-wait: 'sAMAccountName': %s$ out[%s]" % (computer["name"], out))
+            self.assertMatch(out, "# unicodePwd::: REDACTED SECRET ATTRIBUTE",
+                    "getpassword '# unicodePwd::: REDACTED SECRET ATTRIBUTE': out[%s]" % out)
+            self.assertMatch(out, "unicodePwd:: %s" % unicodePwd,
+                    "getpassword unicodePwd: out[%s]" % out)
+            self.assertMatch(out, "# supplementalCredentials::: REDACTED SECRET ATTRIBUTE",
+                    "getpassword '# supplementalCredentials::: REDACTED SECRET ATTRIBUTE': out[%s]" % out)
+            self.assertMatch(out, "supplementalCredentials:: ",
+                    "getpassword supplementalCredentials: out[%s]" % out)
+            if "virtualSambaGPG:: " in out:
+                self.assertMatch(out, "virtualClearTextUTF8:: %s" % virtualClearTextUTF8,
+                    "getpassword virtualClearTextUTF8: out[%s]" % out)
+                self.assertMatch(out, "virtualClearTextUTF16:: %s" % virtualClearTextUTF16,
+                    "getpassword virtualClearTextUTF16: out[%s]" % out)
+                self.assertMatch(out, "virtualSSHA: ",
+                    "getpassword virtualSSHA: out[%s]" % out)
+
+            (result, out, err) = self.runsubcmd(
+                "computer", "getpassword", computer["name"],
+                "--attributes=%s" % attributes,
+                "--decrypt-samba-gpg")
+            self.assertCmdSuccess(result, out, err, "Ensure getpassword runs")
+            self.assertEqual(err,"","getpassword without url")
+            self.assertMatch(out, "Got password OK", "getpassword without url")
+            self.assertMatch(out, "sAMAccountName: %s" % (computer["name"]),
+                    "getpassword: 'sAMAccountName': %s out[%s]" % (computer["name"], out))
+            self.assertMatch(out, "unicodePwd:: %s" % unicodePwd,
+                    "getpassword unicodePwd: out[%s]" % out)
+            self.assertMatch(out, "supplementalCredentials:: ",
+                    "getpassword supplementalCredentials: out[%s]" % out)
+            self._verify_supplementalCredentials(out.replace("\nGot password OK\n", ""))
+            if "virtualSambaGPG:: " in out:
+                self.assertMatch(out, "virtualClearTextUTF8:: %s" % virtualClearTextUTF8,
+                    "getpassword virtualClearTextUTF8: out[%s]" % out)
+                self.assertMatch(out, "virtualClearTextUTF16:: %s" % virtualClearTextUTF16,
+                    "getpassword virtualClearTextUTF16: out[%s]" % out)
+                self.assertMatch(out, "virtualSSHA: ",
+                    "getpassword virtualSSHA: out[%s]" % out)
+
+        for computer in self.computers:
+            newpasswd = self.randomPass()
+            (result, out, err) = self.runsubcmd(
+                "computer", "setpassword",
+                computer["name"],
+                "--newpassword=%s" % newpasswd,
+                "--must-change-at-next-login",
+                "-H", "ldap://%s" % os.environ["DC_SERVER"],
+                "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"]))
+            self.assertCmdSuccess(result, out, err, "Ensure setpassword runs")
+            self.assertEquals(err,"","setpassword with forced change")
+            self.assertMatch(out, "Changed password OK", "setpassword with forced change")
+
+    def test_setexpiry(self):
+        twodays = time.time() + (2 * 24 * 60 * 60)
+
+        for computer in self.computers:
+            (result, out, err) = self.runsubcmd(
+                "computer", "setexpiry", computer["name"],
+                "--days=2",
+                "-H", "ldap://%s" % os.environ["DC_SERVER"],
+                "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"]))
+            self.assertCmdSuccess(result, out, err, "Can we run setexpiry with names")
+            self.assertIn("Expiry for computer '%s' set to 2 days." % computer["name"], out)
+
+        for computer in self.computers:
+            found = self._find_computer(computer["name"])
+
+            expires = nttime2unix(int("%s" % found.get("accountExpires")))
+            self.assertWithin(expires, twodays, 5, "Ensure account expires is within 5 seconds of the expected time")
+
+        # TODO: renable this after the filter case is sorted out
+        if "filters are broken, bail now":
+            return
+
+        # now run the expiration based on a filter
+        fourdays = time.time() + (4 * 24 * 60 * 60)
+        (result, out, err) = self.runsubcmd(
+            "computer", "setexpiry",
+            "--filter", "(&(objectClass=computer)(company=comp2))",
+            "--days=4",
+            "-H", "ldap://%s" % os.environ["DC_SERVER"],
+            "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"]))
+        self.assertCmdSuccess(result, out, err, "Can we run setexpiry with a filter")
+
+        for computer in self.computers:
+            found = self._find_computer(computer["name"])
+            if ("%s" % found.get("company")) == "comp2":
+                expires = nttime2unix(int("%s" % found.get("accountExpires")))
+                self.assertWithin(expires, fourdays, 5, "Ensure account expires is within 5 seconds of the expected time")
+            else:
+                expires = nttime2unix(int("%s" % found.get("accountExpires")))
+                self.assertWithin(expires, twodays, 5, "Ensure account expires is within 5 seconds of the expected time")
+
+
+    def test_list(self):
+        (result, out, err) = self.runsubcmd(
+            "computer", "list",
+            "-H", "ldap://%s" % os.environ["DC_SERVER"],
+            "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"]))
+        self.assertCmdSuccess(result, out, err, "Error running list")
+
+        search_filter = ("(&(objectClass=computer)(userAccountControl:%s:=%u))" %
+                         (ldb.OID_COMPARATOR_AND, dsdb.UF_WORKSTATION_TRUST_ACCOUNT))
+
+        computerlist = self.samdb.search(
+            base=self.samdb.domain_dn(),
+            scope=ldb.SCOPE_SUBTREE,
+            expression=search_filter,
+            attrs=["samaccountname"],
+        )
+
+        self.assertTrue(len(computerlist) > 0, "no computers found in samdb")
+
+        for computerobj in computerlist:
+            name = computerobj.get("samaccountname", idx=0)
+            found = self.assertMatch(out, name,
+                                     "computer '%s' not found" % name)
+
+    def _randomComputer(self, base={}):
+        """create a computer with random attribute values, you can specify base attributes"""
+        computer = {
+            "name": self.randomName(),
+            "password": self.randomPass(),
+            "createComputerFn": self._create_computer,
+            "checkComputerFn": self._check_computer,
+        }
+        computer.update(base)
+        return computer
+
+    def _check_computer(self, computer):
+        """ check if a computer from SamDB has the same attributes as its template """
+        found = self._find_computer(computer["name"])
+        self.assertEquals("%s" % found.get("serviceprincipalname"), computer["service_principal_name"])
+
+    def _create_computer(self, computer):
+        return self.runsubcmd(
+            "computer", "create", computer["name"], computer["password"],
+            "--service-principal-name", computer['service_principal_name'],
+            "-H", "ldap://%s" % os.environ["DC_SERVER"],
+            "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"]),
+        )
+
+    def _find_computer(self, name):
+        search_filter = "(&(sAMAccountName=%s$)(objectCategory=%s,%s))" % (ldb.binary_encode(name), "CN=Computer,CN=Schema,CN=Configuration", self.samdb.domain_dn())
+        computerlist = self.samdb.search(
+            base=self.samdb.domain_dn(),
+            scope=ldb.SCOPE_SUBTREE,
+            expression=search_filter,
+            attrs=[])
+        if computerlist:
+            return computerlist[0]
+        else:
+            return None
diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py
index 80e5c4c..bf05d80 100755
--- a/source4/selftest/tests.py
+++ b/source4/selftest/tests.py
@@ -604,6 +604,7 @@ planpythontestsuite("ad_dc:local", "samba.tests.samba_tool.gpo")
 
 planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.samba_tool.processes")
 planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.samba_tool.user")
+planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.samba_tool.computer")
 planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.samba_tool.user_wdigest")
 planpythontestsuite("ad_dc:local", "samba.tests.samba_tool.user")
 planpythontestsuite("ad_dc:local", "samba.tests.samba_tool.user_virtualCryptSHA")
-- 
2.7.4



More information about the samba-technical mailing list