diff options
author | Björn Baumbach <bb@sernet.de> | 2019-03-19 17:55:37 +0100 |
---|---|---|
committer | Andrew Bartlett <abartlet@samba.org> | 2019-07-04 02:07:21 +0000 |
commit | 3f10c8f25cf1655b4facf190f61acb4098919609 (patch) | |
tree | 8acecd571771a8ca20b0edeb3f27c026a71b8806 /python | |
parent | d103db07b15da86330cc62f21fcb755fca4cfac8 (diff) | |
download | samba-3f10c8f25cf1655b4facf190f61acb4098919609.tar.gz |
samba-tool: implement contact management commands
Usage: samba-tool contact <subcommand>
Contact management.
Available subcommands:
create - Create a new contact.
delete - Delete a contact.
edit - Modify a contact.
list - List all contacts.
move - Move a contact object to an organizational unit or container.
show - Display a contact.
Signed-off-by: Björn Baumbach <bb@samba.org>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
Diffstat (limited to 'python')
-rw-r--r-- | python/samba/netcmd/contact.py | 676 | ||||
-rw-r--r-- | python/samba/netcmd/main.py | 1 | ||||
-rw-r--r-- | python/samba/samdb.py | 108 |
3 files changed, 785 insertions, 0 deletions
diff --git a/python/samba/netcmd/contact.py b/python/samba/netcmd/contact.py new file mode 100644 index 00000000000..506e644c3f8 --- /dev/null +++ b/python/samba/netcmd/contact.py @@ -0,0 +1,676 @@ +# samba-tool contact management +# +# Copyright Bjoern Baumbach 2019 <bbaumbach@samba.org> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import samba.getopt as options +import ldb +import os +import tempfile +from subprocess import check_call, CalledProcessError +from operator import attrgetter +from samba.auth import system_session +from samba.samdb import SamDB +from samba import ( + credentials, + dsdb, +) +from samba.net import Net + +from samba.netcmd import ( + Command, + CommandError, + SuperCommand, + Option, +) +from samba.compat import get_bytes + + +class cmd_create(Command): + """Create a new contact. + + This command creates a new contact in the Active Directory domain. + + The name of the new contact can be specified by the first argument + 'contactname' or the --given-name, --initial and --surname arguments. + If no 'contactname' is given, contact's name will be made up of the given + arguments by combining the given-name, initials and surname. Each argument + is optional. A dot ('.') will be appended to the initials automatically. + + Example1: + samba-tool contact create "James T. Kirk" --job-title=Captain \\ + -H ldap://samba.samdom.example.com -UAdministrator%Passw1rd + + The example shows how to create a new contact in the domain against a remote + LDAP server. + + Example2: + samba-tool contact create --given-name=James --initials=T --surname=Kirk + + The example shows how to create a new contact in the domain against a local + server. The resulting name is "James T. Kirk". + """ + + synopsis = "%prog [contactname] [options]" + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", + type=str, metavar="URL", dest="H"), + Option("--ou", + help=("DN of alternative location (with or without domainDN " + "counterpart) in which the new contact will be created. " + "E.g. 'OU=<OU name>'. " + "Default is the domain base."), + type=str), + Option("--surname", help="Contact's surname", type=str), + Option("--given-name", help="Contact's given name", type=str), + Option("--initials", help="Contact's initials", type=str), + Option("--display-name", help="Contact's display name", type=str), + Option("--job-title", help="Contact's job title", type=str), + Option("--department", help="Contact's department", type=str), + Option("--company", help="Contact's company", type=str), + Option("--description", help="Contact's description", type=str), + Option("--mail-address", help="Contact's email address", type=str), + Option("--internet-address", help="Contact's home page", type=str), + Option("--telephone-number", help="Contact's phone number", type=str), + Option("--mobile-number", + help="Contact's mobile phone number", + type=str), + Option("--physical-delivery-office", + help="Contact's office location", + type=str), + ] + + takes_args = ["fullcontactname?"] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + def run(self, + fullcontactname=None, + sambaopts=None, + credopts=None, + versionopts=None, + H=None, + ou=None, + surname=None, + given_name=None, + initials=None, + display_name=None, + job_title=None, + department=None, + company=None, + description=None, + mail_address=None, + internet_address=None, + telephone_number=None, + mobile_number=None, + physical_delivery_office=None): + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + + try: + samdb = SamDB(url=H, + session_info=system_session(), + credentials=creds, + lp=lp) + ret_name = samdb.newcontact( + fullcontactname=fullcontactname, + ou=ou, + surname=surname, + givenname=given_name, + initials=initials, + displayname=display_name, + jobtitle=job_title, + department=department, + company=company, + description=description, + mailaddress=mail_address, + internetaddress=internet_address, + telephonenumber=telephone_number, + mobilenumber=mobile_number, + physicaldeliveryoffice=physical_delivery_office) + except Exception as e: + raise CommandError("Failed to create contact", e) + + self.outf.write("Contact '%s' created successfully\n" % ret_name) + + +class cmd_delete(Command): + """Delete a contact. + + This command deletes a contact object from the Active Directory domain. + + The contactname specified on the command is the common name or the + distinguished name of the contact object. The distinguished name of the + contact can be specified with or without the domainDN component. + + Example: + samba-tool contact delete Contact1 \\ + -H ldap://samba.samdom.example.com \\ + --username=Administrator --password=Passw1rd + + The example shows how to delete a contact in the domain against a remote + LDAP server. + """ + synopsis = "%prog <contactname> [options]" + + takes_options = [ + Option("-H", + "--URL", + help="LDB URL for database or target server", + type=str, + metavar="URL", + dest="H"), + ] + + takes_args = ["contactname"] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + def run(self, + contactname, + sambaopts=None, + credopts=None, + versionopts=None, + H=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + samdb = SamDB(url=H, + session_info=system_session(), + credentials=creds, + lp=lp) + base_dn = samdb.domain_dn() + scope = ldb.SCOPE_SUBTREE + + filter = ("(&(objectClass=contact)(name=%s))" % + ldb.binary_encode(contactname)) + + if contactname.upper().startswith("CN="): + # contact is specified by DN + filter = "(objectClass=contact)" + scope = ldb.SCOPE_BASE + try: + base_dn = samdb.normalize_dn_in_domain(contactname) + except Exception as e: + raise CommandError('Invalid dn "%s": %s' % + (contactname, e)) + + try: + res = samdb.search(base=base_dn, + scope=scope, + expression=filter, + attrs=["dn"]) + contact_dn = res[0].dn + except IndexError: + raise CommandError('Unable to find contact "%s"' % (contactname)) + + if len(res) > 1: + for msg in sorted(res, key=attrgetter('dn')): + self.outf.write("found: %s\n" % msg.dn) + raise CommandError("Multiple results for contact '%s'\n" + "Please specify the contact's full DN" % + contactname) + + try: + samdb.delete(contact_dn) + except Exception as e: + raise CommandError('Failed to remove contact "%s"' % contactname, e) + self.outf.write("Deleted contact %s\n" % contactname) + + +class cmd_list(Command): + """List all contacts. + """ + + synopsis = "%prog [options]" + + takes_options = [ + Option("-H", + "--URL", + help="LDB URL for database or target server", + type=str, + metavar="URL", + dest="H"), + Option("--full-dn", + dest="full_dn", + default=False, + action='store_true', + help="Display contact's full DN instead of the name."), + ] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + def run(self, + sambaopts=None, + credopts=None, + versionopts=None, + H=None, + full_dn=False): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + + samdb = SamDB(url=H, + session_info=system_session(), + credentials=creds, + lp=lp) + + domain_dn = samdb.domain_dn() + res = samdb.search(domain_dn, + scope=ldb.SCOPE_SUBTREE, + expression="(objectClass=contact)", + attrs=["name"]) + if (len(res) == 0): + return + + if full_dn: + for msg in sorted(res, key=attrgetter('dn')): + self.outf.write("%s\n" % msg.dn) + return + + for msg in res: + contact_name = msg.get("name", idx=0) + + self.outf.write("%s\n" % contact_name) + + +class cmd_edit(Command): + """Modify a contact. + + This command will allow editing of a contact object in the Active Directory + domain. You will then be able to add or change attributes and their values. + + The contactname specified on the command is the common name or the + distinguished name of the contact object. The distinguished name of the + contact can be specified with or without the domainDN component. + + The command may be run from the root userid or another authorized userid. + + The -H or --URL= option can be used to execute the command against a remote + server. + + Example1: + samba-tool contact edit Contact1 -H ldap://samba.samdom.example.com \\ + -U Administrator --password=Passw1rd + + Example1 shows how to edit a contact's attributes in the domain against a + remote LDAP server. + + The -H parameter is used to specify the remote target server. + + Example2: + samba-tool contact edit CN=Contact2,OU=people,DC=samdom,DC=example,DC=com + + Example2 shows how to edit a contact's attributes in the domain against a + local server. The contact, which is located in the 'people' OU, + is specified by the full distinguished name. + + Example3: + samba-tool contact edit Contact3 --editor=nano + + Example3 shows how to edit a contact's attributes in the domain against a + local server using the 'nano' editor. + """ + synopsis = "%prog <contactname> [options]" + + takes_options = [ + Option("-H", + "--URL", + help="LDB URL for database or target server", + type=str, + metavar="URL", + dest="H"), + Option("--editor", + help="Editor to use instead of the system default, " + "or 'vi' if no system default is set.", + type=str), + ] + + takes_args = ["contactname"] + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + def run(self, + contactname, + sambaopts=None, + credopts=None, + versionopts=None, + H=None, + editor=None): + from . import common + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + base_dn = samdb.domain_dn() + scope = ldb.SCOPE_SUBTREE + + filter = ("(&(objectClass=contact)(name=%s))" % + ldb.binary_encode(contactname)) + + if contactname.upper().startswith("CN="): + # contact is specified by DN + filter = "(objectClass=contact)" + scope = ldb.SCOPE_BASE + try: + base_dn = samdb.normalize_dn_in_domain(contactname) + except Exception as e: + raise CommandError('Invalid dn "%s": %s' % + (contactname, e)) + + try: + res = samdb.search(base=base_dn, + scope=scope, + expression=filter) + contact_dn = res[0].dn + except IndexError: + raise CommandError('Unable to find contact "%s"' % (contactname)) + + if len(res) > 1: + for msg in sorted(res, key=attrgetter('dn')): + self.outf.write("found: %s\n" % msg.dn) + raise CommandError("Multiple results for contact '%s'\n" + "Please specify the contact's full DN" % + contactname) + + for msg in res: + result_ldif = common.get_ldif_for_editor(samdb, msg) + + if editor is None: + editor = os.environ.get('EDITOR') + if editor is None: + editor = 'vi' + + with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file: + t_file.write(get_bytes(result_ldif)) + t_file.flush() + try: + check_call([editor, t_file.name]) + except CalledProcessError as e: + raise CalledProcessError("ERROR: ", e) + with open(t_file.name) as edited_file: + edited_message = edited_file.read() + + + msgs_edited = samdb.parse_ldif(edited_message) + msg_edited = next(msgs_edited)[1] + + res_msg_diff = samdb.msg_diff(msg, msg_edited) + if len(res_msg_diff) == 0: + self.outf.write("Nothing to do\n") + return + + try: + samdb.modify(res_msg_diff) + except Exception as e: + raise CommandError("Failed to modify contact '%s': " % contactname, + e) + + self.outf.write("Modified contact '%s' successfully\n" % contactname) + + +class cmd_show(Command): + """Display a contact. + + This command displays a contact object with it's attributes in the Active + Directory domain. + + The contactname specified on the command is the common name or the + distinguished name of the contact object. The distinguished name of the + contact can be specified with or without the domainDN component. + + The command may be run from the root userid or another authorized userid. + + The -H or --URL= option can be used to execute the command against a remote + server. + + Example1: + samba-tool contact show Contact1 -H ldap://samba.samdom.example.com \\ + -U Administrator --password=Passw1rd + + Example1 shows how to display a contact's attributes in the domain against + a remote LDAP server. + + The -H parameter is used to specify the remote target server. + + Example2: + samba-tool contact show CN=Contact2,OU=people,DC=samdom,DC=example,DC=com + + Example2 shows how to display a contact's attributes in the domain against + a local server. The contact, which is located in the 'people' OU, is + specified by the full distinguished name. + + Example3: + samba-tool contact show Contact3 --attributes=mail,mobile + + Example3 shows how to display a contact's mail and mobile attributes. + """ + synopsis = "%prog <contactname> [options]" + + takes_options = [ + Option("-H", + "--URL", + help="LDB URL for database or target server", + type=str, + metavar="URL", + dest="H"), + Option("--attributes", + help=("Comma separated list of attributes, " + "which will be printed."), + type=str, + dest="contact_attrs"), + ] + + takes_args = ["contactname"] + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + def run(self, + contactname, + sambaopts=None, + credopts=None, + versionopts=None, + H=None, + contact_attrs=None): + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + samdb = SamDB(url=H, + session_info=system_session(), + credentials=creds, + lp=lp) + base_dn = samdb.domain_dn() + scope = ldb.SCOPE_SUBTREE + + attrs = None + if contact_attrs: + attrs = contact_attrs.split(",") + + filter = ("(&(objectClass=contact)(name=%s))" % + ldb.binary_encode(contactname)) + + if contactname.upper().startswith("CN="): + # contact is specified by DN + filter = "(objectClass=contact)" + scope = ldb.SCOPE_BASE + try: + base_dn = samdb.normalize_dn_in_domain(contactname) + except Exception as e: + raise CommandError('Invalid dn "%s": %s' % + (contactname, e)) + + try: + res = samdb.search(base=base_dn, + expression=filter, + scope=scope, + attrs=attrs) + contact_dn = res[0].dn + except IndexError: + raise CommandError('Unable to find contact "%s"' % (contactname)) + + if len(res) > 1: + for msg in sorted(res, key=attrgetter('dn')): + self.outf.write("found: %s\n" % msg.dn) + raise CommandError("Multiple results for contact '%s'\n" + "Please specify the contact's DN" % + contactname) + + for msg in res: + contact_ldif = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE) + self.outf.write(contact_ldif) + + +class cmd_move(Command): + """Move a contact object to an organizational unit or container. + + The contactname specified on the command is the common name or the + distinguished name of the contact object. The distinguished name of the + contact can be specified with or without the domainDN component. + + The name of the organizational unit or container can be specified as the + distinguished name, with or without the domainDN component. + + The command may be run from the root userid or another authorized userid. + + The -H or --URL= option can be used to execute the command against a remote + server. + + Example1: + samba-tool contact move Contact1 'OU=people' \\ + -H ldap://samba.samdom.example.com -U Administrator + + Example1 shows how to move a contact Contact1 into the 'people' + organizational unit on a remote LDAP server. + + The -H parameter is used to specify the remote target server. + + Example2: + samba-tool contact move Contact1 OU=Contacts,DC=samdom,DC=example,DC=com + + Example2 shows how to move a contact Contact1 into the OU=Contacts + organizational unit on the local server. + """ + + synopsis = "%prog <contactname> <new_parent_dn> [options]" + + takes_options = [ + Option("-H", + "--URL", + help="LDB URL for database or target server", + type=str, + metavar="URL", + dest="H"), + ] + + takes_args = ["contactname", "new_parent_dn"] + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + def run(self, + contactname, + new_parent_dn, + sambaopts=None, + credopts=None, + versionopts=None, + H=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + samdb = SamDB(url=H, + session_info=system_session(), + credentials=creds, + lp=lp) + base_dn = samdb.domain_dn() + scope = ldb.SCOPE_SUBTREE + + filter = ("(&(objectClass=contact)(name=%s))" % + ldb.binary_encode(contactname)) + + if contactname.upper().startswith("CN="): + # contact is specified by DN + filter = "(objectClass=contact)" + scope = ldb.SCOPE_BASE + try: + base_dn = samdb.normalize_dn_in_domain(contactname) + except Exception as e: + raise CommandError('Invalid dn "%s": %s' % + (contactname, e)) + + try: + res = samdb.search(base=base_dn, + scope=scope, + expression=filter, + attrs=["dn"]) + contact_dn = res[0].dn + except IndexError: + raise CommandError('Unable to find contact "%s"' % (contactname)) + + if len(res) > 1: + for msg in sorted(res, key=attrgetter('dn')): + self.outf.write("found: %s\n" % msg.dn) + raise CommandError("Multiple results for contact '%s'\n" + "Please specify the contact's full DN" % + contactname) + + try: + full_new_parent_dn = samdb.normalize_dn_in_domain(new_parent_dn) + except Exception as e: + raise CommandError('Invalid new_parent_dn "%s": %s' % + (new_parent_dn, e)) + + full_new_contact_dn = ldb.Dn(samdb, str(contact_dn)) + full_new_contact_dn.remove_base_components(len(contact_dn) - 1) + full_new_contact_dn.add_base(full_new_parent_dn) + + try: + samdb.rename(contact_dn, full_new_contact_dn) + except Exception as e: + raise CommandError('Failed to move contact "%s"' % contactname, e) + self.outf.write('Moved contact "%s" into "%s"\n' % + (contactname, full_new_parent_dn)) + + +class cmd_contact(SuperCommand): + """Contact management.""" + + subcommands = {} + subcommands["create"] = cmd_create() + subcommands["delete"] = cmd_delete() + subcommands["edit"] = cmd_edit() + subcommands["list"] = cmd_list() + subcommands["move"] = cmd_move() + subcommands["show"] = cmd_show() diff --git a/python/samba/netcmd/main.py b/python/samba/netcmd/main.py index 261cb78163d..88c33c5aa1a 100644 --- a/python/samba/netcmd/main.py +++ b/python/samba/netcmd/main.py @@ -58,6 +58,7 @@ class cmd_sambatool(SuperCommand): subcommands = cache_loader() subcommands["computer"] = None + subcommands["contact"] = None subcommands["dbcheck"] = None subcommands["delegation"] = None subcommands["dns"] = None diff --git a/python/samba/samdb.py b/python/samba/samdb.py index eda31cb90c3..22819641802 100644 --- a/python/samba/samdb.py +++ b/python/samba/samdb.py @@ -497,6 +497,114 @@ member: %s else: self.transaction_commit() + def newcontact(self, + fullcontactname=None, + ou=None, + surname=None, + givenname=None, + initials=None, + displayname=None, + jobtitle=None, + department=None, + company=None, + description=None, + mailaddress=None, + internetaddress=None, + telephonenumber=None, + mobilenumber=None, + physicaldeliveryoffice=None): + """Adds a new contact with additional parameters + + :param fullcontactname: Optional full name of the new contact + :param ou: Object container for new contact + :param surname: Surname of the new contact + :param givenname: First name of the new contact + :param initials: Initials of the new contact + :param displayname: displayName of the new contact + :param jobtitle: Job title of the new contact + :param department: Department of the new contact + :param company: Company of the new contact + :param description: Description of the new contact + :param mailaddress: Email address of the new contact + :param internetaddress: Home page of the new contact + :param telephonenumber: Phone number of the new contact + :param mobilenumber: Primary mobile number of the new contact + :param physicaldeliveryoffice: Office location of the new contact + """ + + # Prepare the contact name like the RSAT, using the name parts. + cn = "" + if givenname is not None: + cn += givenname + + if initials is not None: + cn += ' %s.' % initials + + if surname is not None: + cn += ' %s' % surname + + # Use the specified fullcontactname instead of the previously prepared + # contact name, if it is specified. + # This is similar to the "Full name" value of the RSAT. + if fullcontactname is not None: + cn = fullcontactname + + if fullcontactname is None and cn == "": + raise Exception('No name for contact specified') + + contactcontainer_dn = self.domain_dn() + if ou: + contactcontainer_dn = self.normalize_dn_in_domain(ou) + + contact_dn = "CN=%s,%s" % (cn, contactcontainer_dn) + + ldbmessage = {"dn": contact_dn, + "objectClass": "contact", + } + + if surname is not None: + ldbmessage["sn"] = surname + + if givenname is not None: + ldbmessage["givenName"] = givenname + + if displayname is not None: + ldbmessage["displayName"] = displayname + + if initials is not None: + ldbmessage["initials"] = '%s.' % initials + + if jobtitle is not None: + ldbmessage["title"] = jobtitle + + if department is not None: + ldbmessage["department"] = department + + if company is not None: + ldbmessage["company"] = company + + if description is not None: + ldbmessage["description"] = description + + if mailaddress is not None: + ldbmessage["mail"] = mailaddress + + if internetaddress is not None: + ldbmessage["wWWHomePage"] = internetaddress + + if telephonenumber is not None: + ldbmessage["telephoneNumber"] = telephonenumber + + if mobilenumber is not None: + ldbmessage["mobile"] = mobilenumber + + if physicaldeliveryoffice is not None: + ldbmessage["physicalDeliveryOfficeName"] = physicaldeliveryoffice + + self.add(ldbmessage) + + return cn + def newcomputer(self, computername, computerou=None, description=None, prepare_oldjoin=False, ip_address_list=None, service_principal_name_list=None): |