1 # samba-tool contact management
3 # Copyright Bjoern Baumbach 2019 <bbaumbach@samba.org>
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 import samba
.getopt
as options
23 from subprocess
import check_call
, CalledProcessError
24 from operator
import attrgetter
25 from samba
.auth
import system_session
26 from samba
.samdb
import SamDB
31 from samba
.net
import Net
33 from samba
.netcmd
import (
39 from samba
.common
import get_bytes
43 class cmd_add(Command
):
46 This command adds a new contact to the Active Directory domain.
48 The name of the new contact can be specified by the first argument
49 'contactname' or the --given-name, --initial and --surname arguments.
50 If no 'contactname' is given, contact's name will be made up of the given
51 arguments by combining the given-name, initials and surname. Each argument
52 is optional. A dot ('.') will be appended to the initials automatically.
55 samba-tool contact add "James T. Kirk" --job-title=Captain \\
56 -H ldap://samba.samdom.example.com -UAdministrator%Passw1rd
58 The example shows how to add a new contact to the domain against a remote
62 samba-tool contact add --given-name=James --initials=T --surname=Kirk
64 The example shows how to add a new contact to the domain against a local
65 server. The resulting name is "James T. Kirk".
68 synopsis
= "%prog [contactname] [options]"
71 Option("-H", "--URL", help="LDB URL for database or target server",
72 type=str, metavar
="URL", dest
="H"),
74 help=("DN of alternative location (with or without domainDN "
75 "counterpart) in which the new contact will be created. "
76 "E.g. 'OU=<OU name>'. "
77 "Default is the domain base."),
79 Option("--surname", help="Contact's surname", type=str),
80 Option("--given-name", help="Contact's given name", type=str),
81 Option("--initials", help="Contact's initials", type=str),
82 Option("--display-name", help="Contact's display name", type=str),
83 Option("--job-title", help="Contact's job title", type=str),
84 Option("--department", help="Contact's department", type=str),
85 Option("--company", help="Contact's company", type=str),
86 Option("--description", help="Contact's description", type=str),
87 Option("--mail-address", help="Contact's email address", type=str),
88 Option("--internet-address", help="Contact's home page", type=str),
89 Option("--telephone-number", help="Contact's phone number", type=str),
90 Option("--mobile-number",
91 help="Contact's mobile phone number",
93 Option("--physical-delivery-office",
94 help="Contact's office location",
98 takes_args
= ["fullcontactname?"]
100 takes_optiongroups
= {
101 "sambaopts": options
.SambaOptions
,
102 "credopts": options
.CredentialsOptions
,
103 "versionopts": options
.VersionOptions
,
107 fullcontactname
=None,
122 internet_address
=None,
123 telephone_number
=None,
125 physical_delivery_office
=None):
127 lp
= sambaopts
.get_loadparm()
128 creds
= credopts
.get_credentials(lp
)
132 session_info
=system_session(),
135 ret_name
= samdb
.newcontact(
136 fullcontactname
=fullcontactname
,
139 givenname
=given_name
,
141 displayname
=display_name
,
143 department
=department
,
145 description
=description
,
146 mailaddress
=mail_address
,
147 internetaddress
=internet_address
,
148 telephonenumber
=telephone_number
,
149 mobilenumber
=mobile_number
,
150 physicaldeliveryoffice
=physical_delivery_office
)
151 except Exception as e
:
152 raise CommandError("Failed to add contact", e
)
154 self
.outf
.write("Contact '%s' added successfully\n" % ret_name
)
157 class cmd_delete(Command
):
160 This command deletes a contact object from the Active Directory domain.
162 The contactname specified on the command is the common name or the
163 distinguished name of the contact object. The distinguished name of the
164 contact can be specified with or without the domainDN component.
167 samba-tool contact delete Contact1 \\
168 -H ldap://samba.samdom.example.com \\
169 --username=Administrator --password=Passw1rd
171 The example shows how to delete a contact in the domain against a remote
174 synopsis
= "%prog <contactname> [options]"
179 help="LDB URL for database or target server",
185 takes_args
= ["contactname"]
187 takes_optiongroups
= {
188 "sambaopts": options
.SambaOptions
,
189 "credopts": options
.CredentialsOptions
,
190 "versionopts": options
.VersionOptions
,
199 lp
= sambaopts
.get_loadparm()
200 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
202 session_info
=system_session(),
205 base_dn
= samdb
.domain_dn()
206 scope
= ldb
.SCOPE_SUBTREE
208 filter = ("(&(objectClass=contact)(name=%s))" %
209 ldb
.binary_encode(contactname
))
211 if contactname
.upper().startswith("CN="):
212 # contact is specified by DN
213 filter = "(objectClass=contact)"
214 scope
= ldb
.SCOPE_BASE
216 base_dn
= samdb
.normalize_dn_in_domain(contactname
)
217 except Exception as e
:
218 raise CommandError('Invalid dn "%s": %s' %
222 res
= samdb
.search(base
=base_dn
,
226 contact_dn
= res
[0].dn
228 raise CommandError('Unable to find contact "%s"' % (contactname
))
231 for msg
in sorted(res
, key
=attrgetter('dn')):
232 self
.outf
.write("found: %s\n" % msg
.dn
)
233 raise CommandError("Multiple results for contact '%s'\n"
234 "Please specify the contact's full DN" %
238 samdb
.delete(contact_dn
)
239 except Exception as e
:
240 raise CommandError('Failed to remove contact "%s"' % contactname
, e
)
241 self
.outf
.write("Deleted contact %s\n" % contactname
)
244 class cmd_list(Command
):
245 """List all contacts.
248 synopsis
= "%prog [options]"
253 help="LDB URL for database or target server",
257 Option("-b", "--base-dn",
258 help="Specify base DN to use.",
264 help="Display contact's full DN instead of the name."),
267 takes_optiongroups
= {
268 "sambaopts": options
.SambaOptions
,
269 "credopts": options
.CredentialsOptions
,
270 "versionopts": options
.VersionOptions
,
280 lp
= sambaopts
.get_loadparm()
281 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
284 session_info
=system_session(),
288 search_dn
= samdb
.domain_dn()
290 search_dn
= samdb
.normalize_dn_in_domain(base_dn
)
292 res
= samdb
.search(search_dn
,
293 scope
=ldb
.SCOPE_SUBTREE
,
294 expression
="(objectClass=contact)",
300 for msg
in sorted(res
, key
=attrgetter('dn')):
301 self
.outf
.write("%s\n" % msg
.dn
)
305 contact_name
= msg
.get("name", idx
=0)
307 self
.outf
.write("%s\n" % contact_name
)
310 class cmd_edit(Command
):
313 This command will allow editing of a contact object in the Active Directory
314 domain. You will then be able to add or change attributes and their values.
316 The contactname specified on the command is the common name or the
317 distinguished name of the contact object. The distinguished name of the
318 contact can be specified with or without the domainDN component.
320 The command may be run from the root userid or another authorized userid.
322 The -H or --URL= option can be used to execute the command against a remote
326 samba-tool contact edit Contact1 -H ldap://samba.samdom.example.com \\
327 -U Administrator --password=Passw1rd
329 Example1 shows how to edit a contact's attributes in the domain against a
332 The -H parameter is used to specify the remote target server.
335 samba-tool contact edit CN=Contact2,OU=people,DC=samdom,DC=example,DC=com
337 Example2 shows how to edit a contact's attributes in the domain against a
338 local server. The contact, which is located in the 'people' OU,
339 is specified by the full distinguished name.
342 samba-tool contact edit Contact3 --editor=nano
344 Example3 shows how to edit a contact's attributes in the domain against a
345 local server using the 'nano' editor.
347 synopsis
= "%prog <contactname> [options]"
352 help="LDB URL for database or target server",
357 help="Editor to use instead of the system default, "
358 "or 'vi' if no system default is set.",
362 takes_args
= ["contactname"]
363 takes_optiongroups
= {
364 "sambaopts": options
.SambaOptions
,
365 "credopts": options
.CredentialsOptions
,
366 "versionopts": options
.VersionOptions
,
376 lp
= sambaopts
.get_loadparm()
377 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
378 samdb
= SamDB(url
=H
, session_info
=system_session(),
379 credentials
=creds
, lp
=lp
)
380 base_dn
= samdb
.domain_dn()
381 scope
= ldb
.SCOPE_SUBTREE
383 filter = ("(&(objectClass=contact)(name=%s))" %
384 ldb
.binary_encode(contactname
))
386 if contactname
.upper().startswith("CN="):
387 # contact is specified by DN
388 filter = "(objectClass=contact)"
389 scope
= ldb
.SCOPE_BASE
391 base_dn
= samdb
.normalize_dn_in_domain(contactname
)
392 except Exception as e
:
393 raise CommandError('Invalid dn "%s": %s' %
397 res
= samdb
.search(base
=base_dn
,
400 contact_dn
= res
[0].dn
402 raise CommandError('Unable to find contact "%s"' % (contactname
))
405 for msg
in sorted(res
, key
=attrgetter('dn')):
406 self
.outf
.write("found: %s\n" % msg
.dn
)
407 raise CommandError("Multiple results for contact '%s'\n"
408 "Please specify the contact's full DN" %
412 result_ldif
= common
.get_ldif_for_editor(samdb
, msg
)
415 editor
= os
.environ
.get('EDITOR')
419 with tempfile
.NamedTemporaryFile(suffix
=".tmp") as t_file
:
420 t_file
.write(get_bytes(result_ldif
))
423 check_call([editor
, t_file
.name
])
424 except CalledProcessError
as e
:
425 raise CalledProcessError("ERROR: ", e
)
426 with
open(t_file
.name
) as edited_file
:
427 edited_message
= edited_file
.read()
430 msgs_edited
= samdb
.parse_ldif(edited_message
)
431 msg_edited
= next(msgs_edited
)[1]
433 res_msg_diff
= samdb
.msg_diff(msg
, msg_edited
)
434 if len(res_msg_diff
) == 0:
435 self
.outf
.write("Nothing to do\n")
439 samdb
.modify(res_msg_diff
)
440 except Exception as e
:
441 raise CommandError("Failed to modify contact '%s': " % contactname
,
444 self
.outf
.write("Modified contact '%s' successfully\n" % contactname
)
447 class cmd_show(Command
):
448 """Display a contact.
450 This command displays a contact object with it's attributes in the Active
453 The contactname specified on the command is the common name or the
454 distinguished name of the contact object. The distinguished name of the
455 contact can be specified with or without the domainDN component.
457 The command may be run from the root userid or another authorized userid.
459 The -H or --URL= option can be used to execute the command against a remote
463 samba-tool contact show Contact1 -H ldap://samba.samdom.example.com \\
464 -U Administrator --password=Passw1rd
466 Example1 shows how to display a contact's attributes in the domain against
467 a remote LDAP server.
469 The -H parameter is used to specify the remote target server.
472 samba-tool contact show CN=Contact2,OU=people,DC=samdom,DC=example,DC=com
474 Example2 shows how to display a contact's attributes in the domain against
475 a local server. The contact, which is located in the 'people' OU, is
476 specified by the full distinguished name.
479 samba-tool contact show Contact3 --attributes=mail,mobile
481 Example3 shows how to display a contact's mail and mobile attributes.
483 synopsis
= "%prog <contactname> [options]"
488 help="LDB URL for database or target server",
492 Option("--attributes",
493 help=("Comma separated list of attributes, "
494 "which will be printed."),
496 dest
="contact_attrs"),
499 takes_args
= ["contactname"]
500 takes_optiongroups
= {
501 "sambaopts": options
.SambaOptions
,
502 "credopts": options
.CredentialsOptions
,
503 "versionopts": options
.VersionOptions
,
514 lp
= sambaopts
.get_loadparm()
515 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
517 session_info
=system_session(),
520 base_dn
= samdb
.domain_dn()
521 scope
= ldb
.SCOPE_SUBTREE
525 attrs
= contact_attrs
.split(",")
527 filter = ("(&(objectClass=contact)(name=%s))" %
528 ldb
.binary_encode(contactname
))
530 if contactname
.upper().startswith("CN="):
531 # contact is specified by DN
532 filter = "(objectClass=contact)"
533 scope
= ldb
.SCOPE_BASE
535 base_dn
= samdb
.normalize_dn_in_domain(contactname
)
536 except Exception as e
:
537 raise CommandError('Invalid dn "%s": %s' %
541 res
= samdb
.search(base
=base_dn
,
545 contact_dn
= res
[0].dn
547 raise CommandError('Unable to find contact "%s"' % (contactname
))
550 for msg
in sorted(res
, key
=attrgetter('dn')):
551 self
.outf
.write("found: %s\n" % msg
.dn
)
552 raise CommandError("Multiple results for contact '%s'\n"
553 "Please specify the contact's DN" %
557 contact_ldif
= common
.get_ldif_for_editor(samdb
, msg
)
558 self
.outf
.write(contact_ldif
)
561 class cmd_move(Command
):
562 """Move a contact object to an organizational unit or container.
564 The contactname specified on the command is the common name or the
565 distinguished name of the contact object. The distinguished name of the
566 contact can be specified with or without the domainDN component.
568 The name of the organizational unit or container can be specified as the
569 distinguished name, with or without the domainDN component.
571 The command may be run from the root userid or another authorized userid.
573 The -H or --URL= option can be used to execute the command against a remote
577 samba-tool contact move Contact1 'OU=people' \\
578 -H ldap://samba.samdom.example.com -U Administrator
580 Example1 shows how to move a contact Contact1 into the 'people'
581 organizational unit on a remote LDAP server.
583 The -H parameter is used to specify the remote target server.
586 samba-tool contact move Contact1 OU=Contacts,DC=samdom,DC=example,DC=com
588 Example2 shows how to move a contact Contact1 into the OU=Contacts
589 organizational unit on the local server.
592 synopsis
= "%prog <contactname> <new_parent_dn> [options]"
597 help="LDB URL for database or target server",
603 takes_args
= ["contactname", "new_parent_dn"]
604 takes_optiongroups
= {
605 "sambaopts": options
.SambaOptions
,
606 "credopts": options
.CredentialsOptions
,
607 "versionopts": options
.VersionOptions
,
617 lp
= sambaopts
.get_loadparm()
618 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
620 session_info
=system_session(),
623 base_dn
= samdb
.domain_dn()
624 scope
= ldb
.SCOPE_SUBTREE
626 filter = ("(&(objectClass=contact)(name=%s))" %
627 ldb
.binary_encode(contactname
))
629 if contactname
.upper().startswith("CN="):
630 # contact is specified by DN
631 filter = "(objectClass=contact)"
632 scope
= ldb
.SCOPE_BASE
634 base_dn
= samdb
.normalize_dn_in_domain(contactname
)
635 except Exception as e
:
636 raise CommandError('Invalid dn "%s": %s' %
640 res
= samdb
.search(base
=base_dn
,
644 contact_dn
= res
[0].dn
646 raise CommandError('Unable to find contact "%s"' % (contactname
))
649 for msg
in sorted(res
, key
=attrgetter('dn')):
650 self
.outf
.write("found: %s\n" % msg
.dn
)
651 raise CommandError("Multiple results for contact '%s'\n"
652 "Please specify the contact's full DN" %
656 full_new_parent_dn
= samdb
.normalize_dn_in_domain(new_parent_dn
)
657 except Exception as e
:
658 raise CommandError('Invalid new_parent_dn "%s": %s' %
661 full_new_contact_dn
= ldb
.Dn(samdb
, str(contact_dn
))
662 full_new_contact_dn
.remove_base_components(len(contact_dn
) - 1)
663 full_new_contact_dn
.add_base(full_new_parent_dn
)
666 samdb
.rename(contact_dn
, full_new_contact_dn
)
667 except Exception as e
:
668 raise CommandError('Failed to move contact "%s"' % contactname
, e
)
669 self
.outf
.write('Moved contact "%s" into "%s"\n' %
670 (contactname
, full_new_parent_dn
))
672 class cmd_rename(Command
):
673 """Rename a contact and related attributes.
675 This command allows to set the contact's name related attributes.
676 The contact's new CN will be made up by combining the given-name, initials
677 and surname. A dot ('.') will be appended to the initials automatically, if
679 Use the --force-new-cn option to specify the new CN manually and the
680 --reset-cn option to reset this changes.
682 Use an empty attribute value to remove the specified attribute.
684 The contactname specified on the command is the CN.
686 The command may be run locally from the root userid or another authorized
689 The -H or --URL= option can be used to execute the command against a remote
693 samba-tool contact rename "John Doe" --surname=Bloggs \\
696 Example1 shows how to change the surname ('sn' attribute) of a contact
697 'John Doe' to 'Bloggs' and change the CN to 'John' on the local server.
700 samba-tool contact rename "J Doe" --given-name=John
701 -H ldap://samba.samdom.example.com -U administrator
703 Example2 shows how to rename the given name of a contact 'J Doe' to
704 'John'. The contact's cn will be renamed automatically, based on
705 the given name, initials and surname, if the previous CN is the
706 standard combination of the previous name attributes.
707 The -H parameter is used to specify the remote target server.
710 synopsis
= "%prog <contactname> [options]"
713 Option("-H", "--URL",
714 help="LDB URL for database or target server",
715 type=str, metavar
="URL", dest
="H"),
719 Option("--given-name",
720 help="New given name",
725 Option("--force-new-cn",
726 help="Specify a new CN (RDN) instead of using a combination "
727 "of the given name, initials and surname.",
728 type=str, metavar
="NEW_CN"),
730 help="Set the CN (RDN) to the combination of the given name, "
731 "initials and surname. Use this option to reset "
732 "the changes made with the --force-new-cn option.",
733 action
="store_true"),
734 Option("--display-name",
735 help="New display name",
737 Option("--mail-address",
738 help="New email address",
742 takes_args
= ["contactname"]
743 takes_optiongroups
= {
744 "sambaopts": options
.SambaOptions
,
745 "credopts": options
.CredentialsOptions
,
746 "versionopts": options
.VersionOptions
,
750 def run(self
, contactname
, credopts
=None, sambaopts
=None, versionopts
=None,
751 H
=None, surname
=None, given_name
=None, initials
=None, force_new_cn
=None,
752 display_name
=None, mail_address
=None, reset_cn
=None):
754 if force_new_cn
and reset_cn
:
755 raise CommandError("It is not allowed to specify --force-new-cn "
756 "together with --reset-cn.")
757 if force_new_cn
== "":
758 raise CommandError("Failed to rename contact - delete protected "
761 lp
= sambaopts
.get_loadparm()
762 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
763 samdb
= SamDB(url
=H
, session_info
=system_session(),
764 credentials
=creds
, lp
=lp
)
765 domain_dn
= ldb
.Dn(samdb
, samdb
.domain_dn())
767 filter = ("(&(objectClass=contact)(name=%s))" %
768 ldb
.binary_encode(contactname
))
770 res
= samdb
.search(base
=domain_dn
,
771 scope
=ldb
.SCOPE_SUBTREE
,
782 contact_dn
= old_contact
.dn
784 raise CommandError('Unable to find contact "%s"' % (contactname
))
786 contact_parent_dn
= contact_dn
.parent()
787 old_cn
= old_contact
["cn"][0]
789 if force_new_cn
is not None:
790 new_cn
= force_new_cn
792 new_cn
= samdb
.fullname_from_names(old_attrs
=old_contact
,
793 given_name
=given_name
,
797 # change CN, if the new CN is different and the old CN is the
798 # standard CN or the change is forced with force-new-cn or reset-cn
799 excepted_cn
= samdb
.fullname_from_names(old_attrs
=old_contact
)
800 must_change_cn
= str(old_cn
) != str(new_cn
) and \
801 (str(old_cn
) == str(excepted_cn
) or \
802 reset_cn
or bool(force_new_cn
))
804 new_contact_dn
= ldb
.Dn(samdb
, "CN=%s" % new_cn
)
805 new_contact_dn
.add_base(contact_parent_dn
)
807 if new_cn
== "" and must_change_cn
:
808 raise CommandError("Failed to rename contact '%s' - "
809 "can not set an empty CN "
810 "(please use --force-new-cn to specify a "
811 "different CN or --given-name, --initials or "
812 "--surname to set name attributes)" % old_cn
)
814 # format given attributes
815 contact_attrs
= ldb
.Message()
816 contact_attrs
.dn
= contact_dn
817 samdb
.prepare_attr_replace(contact_attrs
, old_contact
, "givenName", given_name
)
818 samdb
.prepare_attr_replace(contact_attrs
, old_contact
, "sn", surname
)
819 samdb
.prepare_attr_replace(contact_attrs
, old_contact
, "initials", initials
)
820 samdb
.prepare_attr_replace(contact_attrs
, old_contact
, "displayName", display_name
)
821 samdb
.prepare_attr_replace(contact_attrs
, old_contact
, "mail", mail_address
)
823 contact_attributes_changed
= len(contact_attrs
) > 0
825 # update the contact with formatted attributes
826 samdb
.transaction_start()
828 if contact_attributes_changed
== True:
829 samdb
.modify(contact_attrs
)
831 samdb
.rename(contact_dn
, new_contact_dn
)
832 except Exception as e
:
833 samdb
.transaction_cancel()
834 raise CommandError('Failed to rename contact "%s"' % contactname
, e
)
835 samdb
.transaction_commit()
838 self
.outf
.write('Renamed CN of contact "%s" from "%s" to "%s" '
839 'successfully\n' % (contactname
, old_cn
, new_cn
))
841 if contact_attributes_changed
:
842 self
.outf
.write('Following attributes of contact "%s" have been '
843 'changed successfully:\n' % (contactname
))
844 for attr
in contact_attrs
.keys():
847 self
.outf
.write('%s: %s\n' % (attr
, contact_attrs
[attr
]
848 if contact_attrs
[attr
] else '[removed]'))
850 class cmd_contact(SuperCommand
):
851 """Contact management."""
854 subcommands
["add"] = cmd_add()
855 subcommands
["create"] = cmd_add()
856 subcommands
["delete"] = cmd_delete()
857 subcommands
["edit"] = cmd_edit()
858 subcommands
["list"] = cmd_list()
859 subcommands
["move"] = cmd_move()
860 subcommands
["show"] = cmd_show()
861 subcommands
["rename"] = cmd_rename()