7c3d93a04a1148a1117feafeccb88709fb346a9d
[samba.git] / python / samba / netcmd / user.py
1 # user management
2 #
3 # Copyright Jelmer Vernooij 2010 <jelmer@samba.org>
4 # Copyright Theresa Halloran 2011 <theresahalloran@gmail.com>
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
18 #
19
20 import samba.getopt as options
21 import ldb
22 import pwd
23 import os
24 import sys
25 import errno
26 import base64
27 import binascii
28 from getpass import getpass
29 from samba.auth import system_session
30 from samba.samdb import SamDB
31 from samba.dcerpc import misc
32 from samba.dcerpc import security
33 from samba.dcerpc import drsblobs
34 from samba.ndr import ndr_unpack, ndr_pack, ndr_print
35 from samba import (
36     credentials,
37     dsdb,
38     gensec,
39     generate_random_password,
40     )
41 from samba.net import Net
42
43 from samba.netcmd import (
44     Command,
45     CommandError,
46     SuperCommand,
47     Option,
48     )
49
50 disabled_virtual_attributes = {
51     }
52
53 virtual_attributes = {
54     "virtualClearTextUTF8": {
55         "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
56         },
57     "virtualClearTextUTF16": {
58         "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
59         },
60     }
61
62 get_random_bytes_fn = None
63 if get_random_bytes_fn is None:
64     try:
65         import Crypto.Random
66         get_random_bytes_fn = Crypto.Random.get_random_bytes
67     except ImportError as e:
68         pass
69 if get_random_bytes_fn is None:
70     try:
71         import M2Crypto.Rand
72         get_random_bytes_fn = M2Crypto.Rand.rand_bytes
73     except ImportError as e:
74         pass
75
76 def check_random():
77     if get_random_bytes_fn is not None:
78         return None
79     return "Crypto.Random or M2Crypto.Rand required"
80
81 def get_random_bytes(num):
82     random_reason = check_random()
83     if random_reason is not None:
84         raise ImportError(random_reason)
85     return get_random_bytes_fn(num)
86
87 def get_crypt_value(alg, utf8pw):
88     algs = {
89         "5": {"length": 43},
90         "6": {"length": 86},
91     }
92     assert alg in algs
93     salt = get_random_bytes(16)
94     # The salt needs to be in [A-Za-z0-9./]
95     # base64 is close enough and as we had 16
96     # random bytes but only need 16 characters
97     # we can ignore the possible == at the end
98     # of the base64 string
99     # we just need to replace '+' by '.'
100     b64salt = base64.b64encode(salt)
101     crypt_salt = "$%s$%s$" % (alg, b64salt[0:16].replace('+', '.'))
102     crypt_value = crypt.crypt(utf8pw, crypt_salt)
103     if crypt_value is None:
104         raise NotImplementedError("crypt.crypt(%s) returned None" % (crypt_salt))
105     expected_len = len(crypt_salt) + algs[alg]["length"]
106     if len(crypt_value) != expected_len:
107         raise NotImplementedError("crypt.crypt(%s) returned a value with length %d, expected length is %d" % (
108             crypt_salt, len(crypt_value), expected_len))
109     return crypt_value
110
111 try:
112     random_reason = check_random()
113     if random_reason is not None:
114         raise ImportError(random_reason)
115     import hashlib
116     h = hashlib.sha1()
117     h = None
118     virtual_attributes["virtualSSHA"] = {
119         }
120 except ImportError as e:
121     reason = "hashlib.sha1()"
122     if random_reason:
123         reason += " and " + random_reason
124     reason += " required"
125     disabled_virtual_attributes["virtualSSHA"] = {
126         "reason" : reason,
127         }
128
129 for (alg, attr) in [("5", "virtualCryptSHA256"), ("6", "virtualCryptSHA512")]:
130     try:
131         random_reason = check_random()
132         if random_reason is not None:
133             raise ImportError(random_reason)
134         import crypt
135         v = get_crypt_value(alg, "")
136         v = None
137         virtual_attributes[attr] = {
138             }
139     except ImportError as e:
140         reason = "crypt"
141         if random_reason:
142             reason += " and " + random_reason
143         reason += " required"
144         disabled_virtual_attributes[attr] = {
145             "reason" : reason,
146             }
147     except NotImplementedError as e:
148         reason = "modern '$%s$' salt in crypt(3) required" % (alg)
149         disabled_virtual_attributes[attr] = {
150             "reason" : reason,
151             }
152
153 virtual_attributes_help  = "The attributes to display (comma separated). "
154 virtual_attributes_help += "Possible supported virtual attributes: %s" % ", ".join(sorted(virtual_attributes.keys()))
155 if len(disabled_virtual_attributes) != 0:
156     virtual_attributes_help += "Unsupported virtual attributes: %s" % ", ".join(sorted(disabled_virtual_attributes.keys()))
157
158 class cmd_user_create(Command):
159     """Create a new user.
160
161 This command creates a new user account in the Active Directory domain.  The username specified on the command is the sAMaccountName.
162
163 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).
164
165 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.
166
167 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.
168
169 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.
170
171 Example1:
172 samba-tool user create User1 passw0rd --given-name=John --surname=Smith --must-change-at-next-login -H ldap://samba.samdom.example.com -Uadministrator%passw1rd
173
174 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.
175
176 Example2:
177 sudo samba-tool user create User2 passw2rd --given-name=Jane --surname=Doe --must-change-at-next-login
178
179 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.
180
181 Example3:
182 samba-tool user create User3 passw3rd --userou='OU=OrgUnit'
183
184 Example3 shows how to create a new user in the OrgUnit organizational unit.
185
186 Example4:
187 samba-tool user create User4 passw4rd --rfc2307-from-nss --gecos 'some text'
188
189 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'.
190
191 Example5:
192 samba-tool user create User5 passw5rd --nis-domain=samdom --unix-home=/home/User5 \
193            --uid-number=10005 --login-shell=/bin/false --gid-number=10000
194
195 Example5 shows how to create an RFC2307/NIS domain enabled user account. If
196 --nis-domain is set, then the other four parameters are mandatory.
197
198 """
199     synopsis = "%prog <username> [<password>] [options]"
200
201     takes_options = [
202         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
203                 metavar="URL", dest="H"),
204         Option("--must-change-at-next-login",
205                 help="Force password to be changed on next login",
206                 action="store_true"),
207         Option("--random-password",
208                 help="Generate random password",
209                 action="store_true"),
210         Option("--smartcard-required",
211                 help="Require a smartcard for interactive logons",
212                 action="store_true"),
213         Option("--use-username-as-cn",
214                 help="Force use of username as user's CN",
215                 action="store_true"),
216         Option("--userou",
217                 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>'",
218                 type=str),
219         Option("--surname", help="User's surname", type=str),
220         Option("--given-name", help="User's given name", type=str),
221         Option("--initials", help="User's initials", type=str),
222         Option("--profile-path", help="User's profile path", type=str),
223         Option("--script-path", help="User's logon script path", type=str),
224         Option("--home-drive", help="User's home drive letter", type=str),
225         Option("--home-directory", help="User's home directory path", type=str),
226         Option("--job-title", help="User's job title", type=str),
227         Option("--department", help="User's department", type=str),
228         Option("--company", help="User's company", type=str),
229         Option("--description", help="User's description", type=str),
230         Option("--mail-address", help="User's email address", type=str),
231         Option("--internet-address", help="User's home page", type=str),
232         Option("--telephone-number", help="User's phone number", type=str),
233         Option("--physical-delivery-office", help="User's office location", type=str),
234         Option("--rfc2307-from-nss",
235                 help="Copy Unix user attributes from NSS (will be overridden by explicit UID/GID/GECOS/shell)",
236                 action="store_true"),
237         Option("--nis-domain", help="User's Unix/RFC2307 NIS domain", type=str),
238         Option("--unix-home", help="User's Unix/RFC2307 home directory",
239                 type=str),
240         Option("--uid", help="User's Unix/RFC2307 username", type=str),
241         Option("--uid-number", help="User's Unix/RFC2307 numeric UID", type=int),
242         Option("--gid-number", help="User's Unix/RFC2307 primary GID number", type=int),
243         Option("--gecos", help="User's Unix/RFC2307 GECOS field", type=str),
244         Option("--login-shell", help="User's Unix/RFC2307 login shell", type=str),
245     ]
246
247     takes_args = ["username", "password?"]
248
249     takes_optiongroups = {
250         "sambaopts": options.SambaOptions,
251         "credopts": options.CredentialsOptions,
252         "versionopts": options.VersionOptions,
253         }
254
255     def run(self, username, password=None, credopts=None, sambaopts=None,
256             versionopts=None, H=None, must_change_at_next_login=False,
257             random_password=False, use_username_as_cn=False, userou=None,
258             surname=None, given_name=None, initials=None, profile_path=None,
259             script_path=None, home_drive=None, home_directory=None,
260             job_title=None, department=None, company=None, description=None,
261             mail_address=None, internet_address=None, telephone_number=None,
262             physical_delivery_office=None, rfc2307_from_nss=False,
263             nis_domain=None, unix_home=None, uid=None, uid_number=None,
264             gid_number=None, gecos=None, login_shell=None,
265             smartcard_required=False):
266
267         if smartcard_required:
268             if password is not None and password is not '':
269                 raise CommandError('It is not allowed to specifiy '
270                                    '--newpassword '
271                                    'together with --smartcard-required.')
272             if must_change_at_next_login:
273                 raise CommandError('It is not allowed to specifiy '
274                                    '--must-change-at-next-login '
275                                    'together with --smartcard-required.')
276
277         if random_password and not smartcard_required:
278             password = generate_random_password(128, 255)
279
280         while True:
281             if smartcard_required:
282                 break
283             if password is not None and password is not '':
284                 break
285             password = getpass("New Password: ")
286             passwordverify = getpass("Retype Password: ")
287             if not password == passwordverify:
288                 password = None
289                 self.outf.write("Sorry, passwords do not match.\n")
290
291         if rfc2307_from_nss:
292                 pwent = pwd.getpwnam(username)
293                 if uid is None:
294                     uid = username
295                 if uid_number is None:
296                     uid_number = pwent[2]
297                 if gid_number is None:
298                     gid_number = pwent[3]
299                 if gecos is None:
300                     gecos = pwent[4]
301                 if login_shell is None:
302                     login_shell = pwent[6]
303
304         lp = sambaopts.get_loadparm()
305         creds = credopts.get_credentials(lp)
306
307         if uid_number or gid_number:
308             if not lp.get("idmap_ldb:use rfc2307"):
309                 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")
310
311         if nis_domain is not None:
312             if None in (uid_number, login_shell, unix_home, gid_number):
313                 raise CommandError('Missing parameters. To enable NIS features, '
314                                    'the following options have to be given: '
315                                    '--nis-domain=, --uidNumber=, --login-shell='
316                                    ', --unix-home=, --gid-number= Operation '
317                                    'cancelled.')
318
319         try:
320             samdb = SamDB(url=H, session_info=system_session(),
321                           credentials=creds, lp=lp)
322             samdb.newuser(username, password, force_password_change_at_next_login_req=must_change_at_next_login,
323                           useusernameascn=use_username_as_cn, userou=userou, surname=surname, givenname=given_name, initials=initials,
324                           profilepath=profile_path, homedrive=home_drive, scriptpath=script_path, homedirectory=home_directory,
325                           jobtitle=job_title, department=department, company=company, description=description,
326                           mailaddress=mail_address, internetaddress=internet_address,
327                           telephonenumber=telephone_number, physicaldeliveryoffice=physical_delivery_office,
328                           nisdomain=nis_domain, unixhome=unix_home, uid=uid,
329                           uidnumber=uid_number, gidnumber=gid_number,
330                           gecos=gecos, loginshell=login_shell,
331                           smartcard_required=smartcard_required)
332         except Exception, e:
333             raise CommandError("Failed to add user '%s': " % username, e)
334
335         self.outf.write("User '%s' created successfully\n" % username)
336
337
338 class cmd_user_add(cmd_user_create):
339     __doc__ = cmd_user_create.__doc__
340     # take this print out after the add subcommand is removed.
341     # the add subcommand is deprecated but left in for now to allow people to
342     # migrate to create
343
344     def run(self, *args, **kwargs):
345         self.outf.write(
346             "Note: samba-tool user add is deprecated.  "
347             "Please use samba-tool user create for the same function.\n")
348         return super(cmd_user_add, self).run(*args, **kwargs)
349
350
351 class cmd_user_delete(Command):
352     """Delete a user.
353
354 This command deletes a user account from the Active Directory domain.  The username specified on the command is the sAMAccountName.
355
356 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.
357
358 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.
359
360 Example1:
361 samba-tool user delete User1 -H ldap://samba.samdom.example.com --username=administrator --password=passw1rd
362
363 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.
364
365 Example2:
366 sudo samba-tool user delete User2
367
368 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.
369
370 """
371     synopsis = "%prog <username> [options]"
372
373     takes_options = [
374         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
375                metavar="URL", dest="H"),
376     ]
377
378     takes_args = ["username"]
379     takes_optiongroups = {
380         "sambaopts": options.SambaOptions,
381         "credopts": options.CredentialsOptions,
382         "versionopts": options.VersionOptions,
383         }
384
385     def run(self, username, credopts=None, sambaopts=None, versionopts=None,
386             H=None):
387         lp = sambaopts.get_loadparm()
388         creds = credopts.get_credentials(lp, fallback_machine=True)
389
390         try:
391             samdb = SamDB(url=H, session_info=system_session(),
392                           credentials=creds, lp=lp)
393             samdb.deleteuser(username)
394         except Exception, e:
395             raise CommandError('Failed to remove user "%s"' % username, e)
396         self.outf.write("Deleted user %s\n" % username)
397
398
399 class cmd_user_list(Command):
400     """List all users."""
401
402     synopsis = "%prog [options]"
403
404     takes_options = [
405         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
406                metavar="URL", dest="H"),
407         ]
408
409     takes_optiongroups = {
410         "sambaopts": options.SambaOptions,
411         "credopts": options.CredentialsOptions,
412         "versionopts": options.VersionOptions,
413         }
414
415     def run(self, sambaopts=None, credopts=None, versionopts=None, H=None):
416         lp = sambaopts.get_loadparm()
417         creds = credopts.get_credentials(lp, fallback_machine=True)
418
419         samdb = SamDB(url=H, session_info=system_session(),
420             credentials=creds, lp=lp)
421
422         domain_dn = samdb.domain_dn()
423         res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
424                     expression=("(&(objectClass=user)(userAccountControl:%s:=%u))"
425                     % (ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT)),
426                     attrs=["samaccountname"])
427         if (len(res) == 0):
428             return
429
430         for msg in res:
431             self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
432
433
434 class cmd_user_enable(Command):
435     """Enable an user.
436
437 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.
438
439 There are many reasons why an account may become disabled.  These include:
440 - If a user exceeds the account policy for logon attempts
441 - If an administrator disables the account
442 - If the account expires
443
444 The samba-tool user enable command allows an administrator to enable an account which has become disabled.
445
446 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.
447
448 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.
449
450 Example1:
451 samba-tool user enable Testuser1 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
452
453 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.
454
455 Example2:
456 su samba-tool user enable Testuser2
457
458 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.
459
460 Example3:
461 samba-tool user enable --filter=samaccountname=Testuser3
462
463 Example3 shows how to enable a user in the domain against a local LDAP server.  It uses the --filter=samaccountname to specify the username.
464
465 """
466     synopsis = "%prog (<username>|--filter <filter>) [options]"
467
468
469     takes_optiongroups = {
470         "sambaopts": options.SambaOptions,
471         "versionopts": options.VersionOptions,
472         "credopts": options.CredentialsOptions,
473     }
474
475     takes_options = [
476         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
477                metavar="URL", dest="H"),
478         Option("--filter", help="LDAP Filter to set password on", type=str),
479         ]
480
481     takes_args = ["username?"]
482
483     def run(self, username=None, sambaopts=None, credopts=None,
484             versionopts=None, filter=None, H=None):
485         if username is None and filter is None:
486             raise CommandError("Either the username or '--filter' must be specified!")
487
488         if filter is None:
489             filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
490
491         lp = sambaopts.get_loadparm()
492         creds = credopts.get_credentials(lp, fallback_machine=True)
493
494         samdb = SamDB(url=H, session_info=system_session(),
495             credentials=creds, lp=lp)
496         try:
497             samdb.enable_account(filter)
498         except Exception, msg:
499             raise CommandError("Failed to enable user '%s': %s" % (username or filter, msg))
500         self.outf.write("Enabled user '%s'\n" % (username or filter))
501
502
503 class cmd_user_disable(Command):
504     """Disable an user."""
505
506     synopsis = "%prog (<username>|--filter <filter>) [options]"
507
508     takes_options = [
509         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
510                metavar="URL", dest="H"),
511         Option("--filter", help="LDAP Filter to set password on", type=str),
512         ]
513
514     takes_args = ["username?"]
515
516     takes_optiongroups = {
517         "sambaopts": options.SambaOptions,
518         "credopts": options.CredentialsOptions,
519         "versionopts": options.VersionOptions,
520         }
521
522     def run(self, username=None, sambaopts=None, credopts=None,
523             versionopts=None, filter=None, H=None):
524         if username is None and filter is None:
525             raise CommandError("Either the username or '--filter' must be specified!")
526
527         if filter is None:
528             filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
529
530         lp = sambaopts.get_loadparm()
531         creds = credopts.get_credentials(lp, fallback_machine=True)
532
533         samdb = SamDB(url=H, session_info=system_session(),
534             credentials=creds, lp=lp)
535         try:
536             samdb.disable_account(filter)
537         except Exception, msg:
538             raise CommandError("Failed to disable user '%s': %s" % (username or filter, msg))
539
540
541 class cmd_user_setexpiry(Command):
542     """Set the expiration of a user account.
543
544 The user can either be specified by their sAMAccountName or using the --filter option.
545
546 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.
547
548 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.
549
550 Example1:
551 samba-tool user setexpiry User1 --days=20 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
552
553 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.
554
555 Example2:
556 su samba-tool user setexpiry User2
557
558 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.
559
560 Example3:
561 samba-tool user setexpiry --days=20 --filter=samaccountname=User3
562
563 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.
564
565 Example4:
566 samba-tool user setexpiry --noexpiry User4
567 Example4 shows how to set the account expiration so that it will never expire.  The username and sAMAccountName in this example is User4.
568
569 """
570     synopsis = "%prog (<username>|--filter <filter>) [options]"
571
572     takes_optiongroups = {
573         "sambaopts": options.SambaOptions,
574         "versionopts": options.VersionOptions,
575         "credopts": options.CredentialsOptions,
576     }
577
578     takes_options = [
579         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
580                metavar="URL", dest="H"),
581         Option("--filter", help="LDAP Filter to set password on", type=str),
582         Option("--days", help="Days to expiry", type=int, default=0),
583         Option("--noexpiry", help="Password does never expire", action="store_true", default=False),
584     ]
585
586     takes_args = ["username?"]
587
588     def run(self, username=None, sambaopts=None, credopts=None,
589             versionopts=None, H=None, filter=None, days=None, noexpiry=None):
590         if username is None and filter is None:
591             raise CommandError("Either the username or '--filter' must be specified!")
592
593         if filter is None:
594             filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
595
596         lp = sambaopts.get_loadparm()
597         creds = credopts.get_credentials(lp)
598
599         samdb = SamDB(url=H, session_info=system_session(),
600             credentials=creds, lp=lp)
601
602         try:
603             samdb.setexpiry(filter, days*24*3600, no_expiry_req=noexpiry)
604         except Exception, msg:
605             # FIXME: Catch more specific exception
606             raise CommandError("Failed to set expiry for user '%s': %s" % (
607                 username or filter, msg))
608         if noexpiry:
609             self.outf.write("Expiry for user '%s' disabled.\n" % (
610                 username or filter))
611         else:
612             self.outf.write("Expiry for user '%s' set to %u days.\n" % (
613                 username or filter, days))
614
615
616 class cmd_user_password(Command):
617     """Change password for a user account (the one provided in authentication).
618 """
619
620     synopsis = "%prog [options]"
621
622     takes_options = [
623         Option("--newpassword", help="New password", type=str),
624         ]
625
626     takes_optiongroups = {
627         "sambaopts": options.SambaOptions,
628         "credopts": options.CredentialsOptions,
629         "versionopts": options.VersionOptions,
630         }
631
632     def run(self, credopts=None, sambaopts=None, versionopts=None,
633                 newpassword=None):
634
635         lp = sambaopts.get_loadparm()
636         creds = credopts.get_credentials(lp)
637
638         # get old password now, to get the password prompts in the right order
639         old_password = creds.get_password()
640
641         net = Net(creds, lp, server=credopts.ipaddress)
642
643         password = newpassword
644         while True:
645             if password is not None and password is not '':
646                 break
647             password = getpass("New Password: ")
648             passwordverify = getpass("Retype Password: ")
649             if not password == passwordverify:
650                 password = None
651                 self.outf.write("Sorry, passwords do not match.\n")
652
653         try:
654             net.change_password(password)
655         except Exception, msg:
656             # FIXME: catch more specific exception
657             raise CommandError("Failed to change password : %s" % msg)
658         self.outf.write("Changed password OK\n")
659
660
661 class cmd_user_setpassword(Command):
662     """Set or reset the password of a user account.
663
664 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.
665
666 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.
667
668 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.
669
670 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.
671
672 Example1:
673 samba-tool user setpassword TestUser1 --newpassword=passw0rd --URL=ldap://samba.samdom.example.com -Uadministrator%passw1rd
674
675 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.
676
677 Example2:
678 sudo samba-tool user setpassword TestUser2 --newpassword=passw0rd --must-change-at-next-login
679
680 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.
681
682 Example3:
683 samba-tool user setpassword --filter=samaccountname=TestUser3 --newpassword=passw0rd
684
685 Example3 shows how an administrator would reset TestUser3 user's password to passw0rd using the --filter= option to specify the username.
686
687 """
688     synopsis = "%prog (<username>|--filter <filter>) [options]"
689
690     takes_optiongroups = {
691         "sambaopts": options.SambaOptions,
692         "versionopts": options.VersionOptions,
693         "credopts": options.CredentialsOptions,
694     }
695
696     takes_options = [
697         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
698                metavar="URL", dest="H"),
699         Option("--filter", help="LDAP Filter to set password on", type=str),
700         Option("--newpassword", help="Set password", type=str),
701         Option("--must-change-at-next-login",
702                help="Force password to be changed on next login",
703                action="store_true"),
704         Option("--random-password",
705                 help="Generate random password",
706                 action="store_true"),
707         Option("--smartcard-required",
708                 help="Require a smartcard for interactive logons",
709                 action="store_true"),
710         Option("--clear-smartcard-required",
711                 help="Don't require a smartcard for interactive logons",
712                 action="store_true"),
713         ]
714
715     takes_args = ["username?"]
716
717     def run(self, username=None, filter=None, credopts=None, sambaopts=None,
718             versionopts=None, H=None, newpassword=None,
719             must_change_at_next_login=False, random_password=False,
720             smartcard_required=False, clear_smartcard_required=False):
721         if filter is None and username is None:
722             raise CommandError("Either the username or '--filter' must be specified!")
723
724         password = newpassword
725
726         if smartcard_required:
727             if password is not None and password is not '':
728                 raise CommandError('It is not allowed to specifiy '
729                                    '--newpassword '
730                                    'together with --smartcard-required.')
731             if must_change_at_next_login:
732                 raise CommandError('It is not allowed to specifiy '
733                                    '--must-change-at-next-login '
734                                    'together with --smartcard-required.')
735             if clear_smartcard_required:
736                 raise CommandError('It is not allowed to specifiy '
737                                    '--clear-smartcard-required '
738                                    'together with --smartcard-required.')
739
740         if random_password and not smartcard_required:
741             password = generate_random_password(128, 255)
742
743         while True:
744             if smartcard_required:
745                 break
746             if password is not None and password is not '':
747                 break
748             password = getpass("New Password: ")
749             passwordverify = getpass("Retype Password: ")
750             if not password == passwordverify:
751                 password = None
752                 self.outf.write("Sorry, passwords do not match.\n")
753
754         if filter is None:
755             filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
756
757         lp = sambaopts.get_loadparm()
758         creds = credopts.get_credentials(lp)
759
760         creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
761
762         samdb = SamDB(url=H, session_info=system_session(),
763                       credentials=creds, lp=lp)
764
765         if smartcard_required:
766             command = ""
767             try:
768                 command = "Failed to set UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
769                 flags = dsdb.UF_SMARTCARD_REQUIRED
770                 samdb.toggle_userAccountFlags(filter, flags, on=True)
771                 command = "Failed to enable account for user '%s'" % (username or filter)
772                 samdb.enable_account(filter)
773             except Exception, msg:
774                 # FIXME: catch more specific exception
775                 raise CommandError("%s: %s" % (command, msg))
776             self.outf.write("Added UF_SMARTCARD_REQUIRED OK\n")
777         else:
778             command = ""
779             try:
780                 if clear_smartcard_required:
781                     command = "Failed to remove UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
782                     flags = dsdb.UF_SMARTCARD_REQUIRED
783                     samdb.toggle_userAccountFlags(filter, flags, on=False)
784                 command = "Failed to set password for user '%s'" % (username or filter)
785                 samdb.setpassword(filter, password,
786                                   force_change_at_next_login=must_change_at_next_login,
787                                   username=username)
788             except Exception, msg:
789                 # FIXME: catch more specific exception
790                 raise CommandError("%s: %s" % (command, msg))
791             self.outf.write("Changed password OK\n")
792
793 class GetPasswordCommand(Command):
794
795     def __init__(self):
796         super(GetPasswordCommand, self).__init__()
797         self.lp = None
798
799     def connect_system_samdb(self, url, allow_local=False, verbose=False):
800
801         # using anonymous here, results in no authentication
802         # which means we can get system privileges via
803         # the privileged ldapi socket
804         creds = credentials.Credentials()
805         creds.set_anonymous()
806
807         if url is None and allow_local:
808             pass
809         elif url.lower().startswith("ldapi://"):
810             pass
811         elif url.lower().startswith("ldap://"):
812             raise CommandError("--url ldap:// is not supported for this command")
813         elif url.lower().startswith("ldaps://"):
814             raise CommandError("--url ldaps:// is not supported for this command")
815         elif not allow_local:
816             raise CommandError("--url requires an ldapi:// url for this command")
817
818         if verbose:
819             self.outf.write("Connecting to '%s'\n" % url)
820
821         samdb = SamDB(url=url, session_info=system_session(),
822                       credentials=creds, lp=self.lp)
823
824         try:
825             #
826             # Make sure we're connected as SYSTEM
827             #
828             res = samdb.search(base='', scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
829             assert len(res) == 1
830             sids = res[0].get("tokenGroups")
831             assert len(sids) == 1
832             sid = ndr_unpack(security.dom_sid, sids[0])
833             assert str(sid) == security.SID_NT_SYSTEM
834         except Exception as msg:
835             raise CommandError("You need to specify an URL that gives privileges as SID_NT_SYSTEM(%s)" %
836                                (security.SID_NT_SYSTEM))
837
838         # We use sort here in order to have a predictable processing order
839         # this might not be strictly needed, but also doesn't hurt here
840         for a in sorted(virtual_attributes.keys()):
841             flags = ldb.ATTR_FLAG_HIDDEN | virtual_attributes[a].get("flags", 0)
842             samdb.schema_attribute_add(a, flags, ldb.SYNTAX_OCTET_STRING)
843
844         return samdb
845
846     def get_account_attributes(self, samdb, username,
847                                basedn, filter, scope, attrs):
848
849         require_supplementalCredentials = False
850         search_attrs = attrs[:]
851         lower_attrs = [x.lower() for x in search_attrs]
852         for a in virtual_attributes.keys():
853             if a.lower() in lower_attrs:
854                 require_supplementalCredentials = True
855         add_supplementalCredentials = False
856         if require_supplementalCredentials:
857             a = "supplementalCredentials"
858             if a.lower() not in lower_attrs:
859                 search_attrs += [a]
860                 add_supplementalCredentials = True
861         add_sAMAcountName = False
862         a = "sAMAccountName"
863         if a.lower() not in lower_attrs:
864             search_attrs += [a]
865             add_sAMAcountName = True
866
867         if scope == ldb.SCOPE_BASE:
868             search_controls = ["show_deleted:1", "show_recycled:1"]
869         else:
870             search_controls = []
871         try:
872             res = samdb.search(base=basedn, expression=filter,
873                                scope=scope, attrs=search_attrs,
874                                controls=search_controls)
875             if len(res) == 0:
876                 raise Exception('Unable to find user "%s"' % (username or filter))
877             if len(res) > 1:
878                 raise Exception('Matched %u multiple users with filter "%s"' % (len(res), filter))
879         except Exception as msg:
880             # FIXME: catch more specific exception
881             raise CommandError("Failed to get password for user '%s': %s" % (username or filter, msg))
882         obj = res[0]
883
884         sc = None
885         if "supplementalCredentials" in obj:
886             sc_blob = obj["supplementalCredentials"][0]
887             sc = ndr_unpack(drsblobs.supplementalCredentialsBlob, sc_blob)
888             if add_supplementalCredentials:
889                 del obj["supplementalCredentials"]
890         account_name = obj["sAMAccountName"][0]
891         if add_sAMAcountName:
892             del obj["sAMAccountName"]
893
894         def get_package(name):
895             if sc is None:
896                 return None
897             for p in sc.sub.packages:
898                 if name != p.name:
899                     continue
900
901                 return binascii.a2b_hex(p.data)
902             return None
903
904         def get_utf8(a, b, username):
905             try:
906                 u = unicode(b, 'utf-16-le')
907             except UnicodeDecodeError as e:
908                 self.outf.write("WARNING: '%s': CLEARTEXT is invalid UTF-16-LE unable to generate %s\n" % (
909                                 username, a))
910                 return None
911             u8 = u.encode('utf-8')
912             return u8
913
914         # We use sort here in order to have a predictable processing order
915         for a in sorted(virtual_attributes.keys()):
916             if not a.lower() in lower_attrs:
917                 continue
918
919             if a == "virtualClearTextUTF8":
920                 b = get_package("Primary:CLEARTEXT")
921                 if b is None:
922                     continue
923                 u8 = get_utf8(a, b, username or account_name)
924                 if u8 is None:
925                     continue
926                 v = u8
927             elif a == "virtualClearTextUTF16":
928                 v = get_package("Primary:CLEARTEXT")
929                 if v is None:
930                     continue
931             elif a == "virtualSSHA":
932                 b = get_package("Primary:CLEARTEXT")
933                 if b is None:
934                     continue
935                 u8 = get_utf8(a, b, username or account_name)
936                 if u8 is None:
937                     continue
938                 salt = get_random_bytes(4)
939                 h = hashlib.sha1()
940                 h.update(u8)
941                 h.update(salt)
942                 bv = h.digest() + salt
943                 v = "{SSHA}" + base64.b64encode(bv)
944             elif a == "virtualCryptSHA256":
945                 b = get_package("Primary:CLEARTEXT")
946                 if b is None:
947                     continue
948                 u8 = get_utf8(a, b, username or account_name)
949                 if u8 is None:
950                     continue
951                 sv = get_crypt_value("5", u8)
952                 v = "{CRYPT}" + sv
953             elif a == "virtualCryptSHA512":
954                 b = get_package("Primary:CLEARTEXT")
955                 if b is None:
956                     continue
957                 u8 = get_utf8(a, b, username or account_name)
958                 if u8 is None:
959                     continue
960                 sv = get_crypt_value("6", u8)
961                 v = "{CRYPT}" + sv
962             else:
963                 continue
964             obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, a)
965         return obj
966
967     def parse_attributes(self, attributes):
968
969         if attributes is None:
970             raise CommandError("Please specify --attributes")
971         attrs = attributes.split(',')
972         password_attrs = []
973         for pa in attrs:
974             pa = pa.lstrip().rstrip()
975             for da in disabled_virtual_attributes.keys():
976                 if pa.lower() == da.lower():
977                     r = disabled_virtual_attributes[da]["reason"]
978                     raise CommandError("Virtual attribute '%s' not supported: %s" % (
979                                        da, r))
980             for va in virtual_attributes.keys():
981                 if pa.lower() == va.lower():
982                     # Take the real name
983                     pa = va
984                     break
985             password_attrs += [pa]
986
987         return password_attrs
988
989 class cmd_user_getpassword(GetPasswordCommand):
990     """Get the password fields of a user/computer account.
991
992 This command gets the logon password for a user/computer account.
993
994 The username specified on the command is the sAMAccountName.
995 The username may also be specified using the --filter option.
996
997 The command must be run from the root user id or another authorized user id.
998 The '-H' or '--URL' option only supports ldapi:// or [tdb://] and can be
999 used to adjust the local path. By default tdb:// is used by default.
1000
1001 The '--attributes' parameter takes a comma separated list of attributes,
1002 which will be printed or given to the script specified by '--script'. If a
1003 specified attribute is not available on an object it's silently omitted.
1004 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1005 the NTHASH) and the following virtual attributes are possible (see --help
1006 for which virtual attributes are supported in your environment):
1007
1008    virtualClearTextUTF16: The raw cleartext as stored in the
1009                           'Primary:CLEARTEXT' buffer inside of the
1010                           supplementalCredentials attribute. This typically
1011                           contains valid UTF-16-LE, but may contain random
1012                           bytes, e.g. for computer accounts.
1013
1014    virtualClearTextUTF8:  As virtualClearTextUTF16, but converted to UTF-8
1015                           (only from valid UTF-16-LE)
1016
1017    virtualSSHA:           As virtualClearTextUTF8, but a salted SHA-1
1018                           checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1019
1020    virtualCryptSHA256:    As virtualClearTextUTF8, but a salted SHA256
1021                           checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1022                           with a $5$... salt, see crypt(3) on modern systems.
1023
1024    virtualCryptSHA512:    As virtualClearTextUTF8, but a salted SHA512
1025                           checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1026                           with a $6$... salt, see crypt(3) on modern systems.
1027
1028 Example1:
1029 samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8
1030
1031 Example2:
1032 samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-KeyVersionNumber,unicodePwd,virtualClearTextUTF16
1033
1034 """
1035     def __init__(self):
1036         super(cmd_user_getpassword, self).__init__()
1037
1038     synopsis = "%prog (<username>|--filter <filter>) [options]"
1039
1040     takes_optiongroups = {
1041         "sambaopts": options.SambaOptions,
1042         "versionopts": options.VersionOptions,
1043     }
1044
1045     takes_options = [
1046         Option("-H", "--URL", help="LDB URL for sam.ldb database or local ldapi server", type=str,
1047                metavar="URL", dest="H"),
1048         Option("--filter", help="LDAP Filter to set password on", type=str),
1049         Option("--attributes", type=str,
1050                help=virtual_attributes_help,
1051                metavar="ATTRIBUTELIST", dest="attributes"),
1052         ]
1053
1054     takes_args = ["username?"]
1055
1056     def run(self, username=None, H=None, filter=None,
1057             attributes=None,
1058             sambaopts=None, versionopts=None):
1059         self.lp = sambaopts.get_loadparm()
1060
1061         if filter is None and username is None:
1062             raise CommandError("Either the username or '--filter' must be specified!")
1063
1064         if filter is None:
1065             filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
1066
1067         if attributes is None:
1068             raise CommandError("Please specify --attributes")
1069
1070         password_attrs = self.parse_attributes(attributes)
1071
1072         samdb = self.connect_system_samdb(url=H, allow_local=True)
1073
1074         obj = self.get_account_attributes(samdb, username,
1075                                           basedn=None,
1076                                           filter=filter,
1077                                           scope=ldb.SCOPE_SUBTREE,
1078                                           attrs=password_attrs)
1079
1080         ldif = samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1081         self.outf.write("%s" % ldif)
1082         self.outf.write("Got password OK\n")
1083
1084 class cmd_user(SuperCommand):
1085     """User management."""
1086
1087     subcommands = {}
1088     subcommands["add"] = cmd_user_add()
1089     subcommands["create"] = cmd_user_create()
1090     subcommands["delete"] = cmd_user_delete()
1091     subcommands["disable"] = cmd_user_disable()
1092     subcommands["enable"] = cmd_user_enable()
1093     subcommands["list"] = cmd_user_list()
1094     subcommands["setexpiry"] = cmd_user_setexpiry()
1095     subcommands["password"] = cmd_user_password()
1096     subcommands["setpassword"] = cmd_user_setpassword()
1097     subcommands["getpassword"] = cmd_user_getpassword()