ctdb-scripts: Improve update and listing code
[samba4-gss.git] / python / samba / netcmd / pso.py
blobd260e3bd4064082cc65c9b95135a7135b653d748
1 # Manages Password Settings Objects
3 # Copyright (C) Andrew Bartlett <abartlet@samba.org> 2018
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/>.
18 import samba.getopt as options
19 import ldb
20 from samba.samdb import SamDB
21 from samba.netcmd import (Command, CommandError, Option, SuperCommand)
22 from samba.dcerpc.samr import (DOMAIN_PASSWORD_COMPLEX,
23 DOMAIN_PASSWORD_STORE_CLEARTEXT)
24 from samba.auth import system_session
25 from samba.netcmd.common import (NEVER_TIMESTAMP,
26 timestamp_to_mins,
27 timestamp_to_days)
30 def pso_container(samdb):
31 return "CN=Password Settings Container,CN=System,%s" % samdb.domain_dn()
34 def mins_to_timestamp(mins):
35 """Converts a value in minutes to -100 nanosecond units"""
36 timestamp = -int((1e7) * 60 * mins)
37 return str(timestamp)
40 def days_to_timestamp(days):
41 """Converts a value in days to -100 nanosecond units"""
42 timestamp = mins_to_timestamp(days * 60 * 24)
43 return str(timestamp)
46 def show_pso_by_dn(outf, samdb, dn, show_applies_to=True):
47 """Displays the password settings for a PSO specified by DN"""
49 # map from the boolean LDB value to the CLI string the user sees
50 on_off_str = {"TRUE": "on", "FALSE": "off"}
52 pso_attrs = ['name', 'msDS-PasswordSettingsPrecedence',
53 'msDS-PasswordReversibleEncryptionEnabled',
54 'msDS-PasswordHistoryLength', 'msDS-MinimumPasswordLength',
55 'msDS-PasswordComplexityEnabled', 'msDS-MinimumPasswordAge',
56 'msDS-MaximumPasswordAge', 'msDS-LockoutObservationWindow',
57 'msDS-LockoutThreshold', 'msDS-LockoutDuration',
58 'msDS-PSOAppliesTo']
60 res = samdb.search(dn, scope=ldb.SCOPE_BASE, attrs=pso_attrs)
61 pso_res = res[0]
62 outf.write("Password information for PSO '%s'\n" % pso_res['name'])
63 outf.write("\n")
65 outf.write("Precedence (lowest is best): %s\n" %
66 pso_res['msDS-PasswordSettingsPrecedence'])
67 bool_str = str(pso_res['msDS-PasswordComplexityEnabled'])
68 outf.write("Password complexity: %s\n" % on_off_str[bool_str])
69 bool_str = str(pso_res['msDS-PasswordReversibleEncryptionEnabled'])
70 outf.write("Store plaintext passwords: %s\n" % on_off_str[bool_str])
71 outf.write("Password history length: %s\n" %
72 pso_res['msDS-PasswordHistoryLength'])
73 outf.write("Minimum password length: %s\n" %
74 pso_res['msDS-MinimumPasswordLength'])
75 outf.write("Minimum password age (days): %d\n" %
76 timestamp_to_days(pso_res['msDS-MinimumPasswordAge'][0]))
77 outf.write("Maximum password age (days): %d\n" %
78 timestamp_to_days(pso_res['msDS-MaximumPasswordAge'][0]))
79 outf.write("Account lockout duration (mins): %d\n" %
80 timestamp_to_mins(pso_res['msDS-LockoutDuration'][0]))
81 outf.write("Account lockout threshold (attempts): %s\n" %
82 pso_res['msDS-LockoutThreshold'])
83 outf.write("Reset account lockout after (mins): %d\n" %
84 timestamp_to_mins(pso_res['msDS-LockoutObservationWindow'][0]))
86 if show_applies_to:
87 if 'msDS-PSOAppliesTo' in pso_res:
88 outf.write("\nPSO applies directly to %d groups/users:\n" %
89 len(pso_res['msDS-PSOAppliesTo']))
90 for dn in pso_res['msDS-PSOAppliesTo']:
91 outf.write(" %s\n" % dn)
92 else:
93 outf.write("\nNote: PSO does not apply to any users or groups.\n")
96 def check_pso_valid(samdb, pso_dn, name):
97 """Gracefully bail out if we can't view/modify the PSO specified"""
98 # the base scope search for the PSO throws an error if it doesn't exist
99 try:
100 res = samdb.search(pso_dn, scope=ldb.SCOPE_BASE,
101 attrs=['msDS-PasswordSettingsPrecedence'])
102 except ldb.LdbError as e:
103 if e.args[0] == ldb.ERR_NO_SUCH_OBJECT:
104 raise CommandError("Unable to find PSO '%s'" % name)
105 raise
107 # users need admin permission to modify/view a PSO. In this case, the
108 # search succeeds, but it doesn't return any attributes
109 if 'msDS-PasswordSettingsPrecedence' not in res[0]:
110 raise CommandError("You may not have permission to view/modify PSOs")
113 def show_pso_for_user(outf, samdb, username):
114 """Displays the password settings for a specific user"""
116 search_filter = "(&(sAMAccountName=%s)(objectClass=user))" % username
118 res = samdb.search(samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE,
119 expression=search_filter,
120 attrs=['msDS-ResultantPSO', 'msDS-PSOApplied'])
122 if len(res) == 0:
123 outf.write("User '%s' not found.\n" % username)
124 elif 'msDS-ResultantPSO' not in res[0]:
125 outf.write("No PSO applies to user '%s'. "
126 "The default domain settings apply.\n" % username)
127 outf.write("Refer to 'samba-tool domain passwordsettings show'.\n")
128 else:
129 # sanity-check user has permissions to view PSO details (non-admin
130 # users can view msDS-ResultantPSO, but not the actual PSO details)
131 check_pso_valid(samdb, res[0]['msDS-ResultantPSO'][0], "???")
132 outf.write("The following PSO settings apply to user '%s'.\n\n" %
133 username)
134 show_pso_by_dn(outf, samdb, res[0]['msDS-ResultantPSO'][0],
135 show_applies_to=False)
136 # PSOs that apply directly to a user don't necessarily have the best
137 # precedence, which could be a little confusing for PSO management
138 if 'msDS-PSOApplied' in res[0]:
139 outf.write("\nNote: PSO applies directly to user "
140 "(any group PSOs are overridden)\n")
141 else:
142 outf.write("\nPSO applies to user via group membership.\n")
145 def msg_add_attr(msg, attr_name, value, ldb_oper):
146 msg[attr_name] = ldb.MessageElement(value, ldb_oper, attr_name)
149 def make_pso_ldb_msg(outf, samdb, pso_dn, create, lockout_threshold=None,
150 complexity=None, precedence=None, store_plaintext=None,
151 history_length=None, min_pwd_length=None,
152 min_pwd_age=None, max_pwd_age=None, lockout_duration=None,
153 reset_lockout_after=None):
154 """Packs the given PSO settings into an LDB message"""
156 m = ldb.Message()
157 m.dn = ldb.Dn(samdb, pso_dn)
159 if create:
160 ldb_oper = ldb.FLAG_MOD_ADD
161 m["msDS-objectClass"] = ldb.MessageElement("msDS-PasswordSettings",
162 ldb_oper, "objectClass")
163 else:
164 ldb_oper = ldb.FLAG_MOD_REPLACE
166 if precedence is not None:
167 msg_add_attr(m, "msDS-PasswordSettingsPrecedence", str(precedence),
168 ldb_oper)
170 if complexity is not None:
171 bool_str = "TRUE" if complexity == "on" else "FALSE"
172 msg_add_attr(m, "msDS-PasswordComplexityEnabled", bool_str, ldb_oper)
174 if store_plaintext is not None:
175 bool_str = "TRUE" if store_plaintext == "on" else "FALSE"
176 msg_add_attr(m, "msDS-PasswordReversibleEncryptionEnabled",
177 bool_str, ldb_oper)
179 if history_length is not None:
180 msg_add_attr(m, "msDS-PasswordHistoryLength", str(history_length),
181 ldb_oper)
183 if min_pwd_length is not None:
184 msg_add_attr(m, "msDS-MinimumPasswordLength", str(min_pwd_length),
185 ldb_oper)
187 if min_pwd_age is not None:
188 min_pwd_age_ticks = days_to_timestamp(min_pwd_age)
189 msg_add_attr(m, "msDS-MinimumPasswordAge", min_pwd_age_ticks,
190 ldb_oper)
192 if max_pwd_age is not None:
193 # Windows won't let you set max-pwd-age to zero. Here we take zero to
194 # mean 'never expire' and use the timestamp corresponding to 'never'
195 if max_pwd_age == 0:
196 max_pwd_age_ticks = str(NEVER_TIMESTAMP)
197 else:
198 max_pwd_age_ticks = days_to_timestamp(max_pwd_age)
199 msg_add_attr(m, "msDS-MaximumPasswordAge", max_pwd_age_ticks, ldb_oper)
201 if lockout_duration is not None:
202 lockout_duration_ticks = mins_to_timestamp(lockout_duration)
203 msg_add_attr(m, "msDS-LockoutDuration", lockout_duration_ticks,
204 ldb_oper)
206 if lockout_threshold is not None:
207 msg_add_attr(m, "msDS-LockoutThreshold", str(lockout_threshold),
208 ldb_oper)
210 if reset_lockout_after is not None:
211 msg_add_attr(m, "msDS-LockoutObservationWindow",
212 mins_to_timestamp(reset_lockout_after), ldb_oper)
214 return m
217 def check_pso_constraints(min_pwd_length=None, history_length=None,
218 min_pwd_age=None, max_pwd_age=None):
219 """Checks PSO settings fall within valid ranges"""
221 # check values as per section 3.1.1.5.2.2 Constraints in MS-ADTS spec
222 if history_length is not None and history_length > 1024:
223 raise CommandError("Bad password history length: "
224 "valid range is 0 to 1024")
226 if min_pwd_length is not None and min_pwd_length > 255:
227 raise CommandError("Bad minimum password length: "
228 "valid range is 0 to 255")
230 if min_pwd_age is not None and max_pwd_age is not None:
231 # note max-age=zero is a special case meaning 'never expire'
232 if min_pwd_age >= max_pwd_age and max_pwd_age != 0:
233 raise CommandError("Minimum password age must be less than "
234 "maximum age")
237 # the same args are used for both create and set commands
238 pwd_settings_options = [
239 Option("--complexity", type="choice", choices=["on", "off"],
240 help="The password complexity (on | off)."),
241 Option("--store-plaintext", type="choice", choices=["on", "off"],
242 help="Store plaintext passwords where account have "
243 "'store passwords with reversible encryption' set (on | off)."),
244 Option("--history-length",
245 help="The password history length (<integer>).", type=int),
246 Option("--min-pwd-length",
247 help="The minimum password length (<integer>).", type=int),
248 Option("--min-pwd-age",
249 help=("The minimum password age (<integer in days>). "
250 "Default is domain setting."), type=int),
251 Option("--max-pwd-age",
252 help=("The maximum password age (<integer in days>). "
253 "Default is domain setting."), type=int),
254 Option("--account-lockout-duration", type=int,
255 help=("The length of time an account is locked out after exceeding "
256 "the limit on bad password attempts (<integer in mins>). "
257 "Default is domain setting")),
258 Option("--account-lockout-threshold", type=int,
259 help=("The number of bad password attempts allowed before locking "
260 "out the account (<integer>). Default is domain setting.")),
261 Option("--reset-account-lockout-after",
262 help=("After this time is elapsed, the recorded number of attempts "
263 "restarts from zero (<integer in mins>). "
264 "Default is domain setting."), type=int)]
267 def num_options_in_args(options, args):
269 Returns the number of options specified that are present in the args.
270 (There can be other args besides just the ones we're interested in, which
271 is why argc on its own is not enough)
273 num_opts = 0
274 for opt in options:
275 for arg in args:
276 # The option should be a sub-string of the CLI argument for a match
277 if str(opt) in arg:
278 num_opts += 1
279 return num_opts
282 class cmd_domain_pwdsettings_pso_create(Command):
283 """Creates a new Password Settings Object (PSO).
285 PSOs are a way to tailor different password settings (lockout policy,
286 minimum password length, etc) for specific users or groups.
288 The psoname is a unique name for the new Password Settings Object.
289 When multiple PSOs apply to a user, the precedence determines which PSO
290 will take effect. The PSO with the lowest precedence will take effect.
292 For most arguments, the default value (if unspecified) is the current
293 domain passwordsettings value. To see these values, enter the command
294 'samba-tool domain passwordsettings show'.
296 To apply the new PSO to user(s) or group(s), enter the command
297 'samba-tool domain passwordsettings pso apply'.
300 synopsis = "%prog <psoname> <precedence> [options]"
302 takes_optiongroups = {
303 "sambaopts": options.SambaOptions,
304 "versionopts": options.VersionOptions,
305 "credopts": options.CredentialsOptions,
308 takes_options = pwd_settings_options + [
309 Option("-H", "--URL", help="LDB URL for database or target server",
310 metavar="URL", dest="H", type=str)
312 takes_args = ["psoname", "precedence"]
314 def run(self, psoname, precedence, H=None, min_pwd_age=None,
315 max_pwd_age=None, complexity=None, store_plaintext=None,
316 history_length=None, min_pwd_length=None,
317 account_lockout_duration=None, account_lockout_threshold=None,
318 reset_account_lockout_after=None, credopts=None, sambaopts=None,
319 versionopts=None):
320 lp = sambaopts.get_loadparm()
321 creds = credopts.get_credentials(lp)
323 samdb = SamDB(url=H, session_info=system_session(),
324 credentials=creds, lp=lp)
326 try:
327 precedence = int(precedence)
328 except ValueError:
329 raise CommandError("The PSO's precedence should be "
330 "a numerical value. Try --help")
332 # sanity-check that the PSO doesn't already exist
333 pso_dn = "CN=%s,%s" % (psoname, pso_container(samdb))
334 try:
335 res = samdb.search(pso_dn, scope=ldb.SCOPE_BASE)
336 except ldb.LdbError as e:
337 if e.args[0] == ldb.ERR_NO_SUCH_OBJECT:
338 pass
339 else:
340 raise
341 else:
342 raise CommandError("PSO '%s' already exists" % psoname)
344 # we expect the user to specify at least one password-policy setting,
345 # otherwise there's no point in creating a PSO
346 num_pwd_args = num_options_in_args(pwd_settings_options, self.raw_argv)
347 if num_pwd_args == 0:
348 raise CommandError("Please specify at least one password policy "
349 "setting. Try --help")
351 # it's unlikely that the user will specify all 9 password policy
352 # settings on the CLI - current domain password-settings as the default
353 # values for unspecified arguments
354 if num_pwd_args < len(pwd_settings_options):
355 self.message("Not all password policy options "
356 "have been specified.")
357 self.message("For unspecified options, the current domain password"
358 " settings will be used as the default values.")
360 # lookup the current domain password-settings
361 res = samdb.search(samdb.domain_dn(), scope=ldb.SCOPE_BASE,
362 attrs=["pwdProperties", "pwdHistoryLength", "minPwdLength",
363 "minPwdAge", "maxPwdAge", "lockoutDuration",
364 "lockoutThreshold", "lockOutObservationWindow"])
365 assert(len(res) == 1)
367 # use the domain settings for any missing arguments
368 pwd_props = int(res[0]["pwdProperties"][0])
369 if complexity is None:
370 prop_flag = DOMAIN_PASSWORD_COMPLEX
371 complexity = "on" if pwd_props & prop_flag else "off"
373 if store_plaintext is None:
374 prop_flag = DOMAIN_PASSWORD_STORE_CLEARTEXT
375 store_plaintext = "on" if pwd_props & prop_flag else "off"
377 if history_length is None:
378 history_length = int(res[0]["pwdHistoryLength"][0])
380 if min_pwd_length is None:
381 min_pwd_length = int(res[0]["minPwdLength"][0])
383 if min_pwd_age is None:
384 min_pwd_age = timestamp_to_days(res[0]["minPwdAge"][0])
386 if max_pwd_age is None:
387 max_pwd_age = timestamp_to_days(res[0]["maxPwdAge"][0])
389 if account_lockout_duration is None:
390 account_lockout_duration = \
391 timestamp_to_mins(res[0]["lockoutDuration"][0])
393 if account_lockout_threshold is None:
394 account_lockout_threshold = int(res[0]["lockoutThreshold"][0])
396 if reset_account_lockout_after is None:
397 reset_account_lockout_after = \
398 timestamp_to_mins(res[0]["lockOutObservationWindow"][0])
400 check_pso_constraints(max_pwd_age=max_pwd_age, min_pwd_age=min_pwd_age,
401 history_length=history_length,
402 min_pwd_length=min_pwd_length)
404 # pack the settings into an LDB message
405 m = make_pso_ldb_msg(self.outf, samdb, pso_dn, create=True,
406 complexity=complexity, precedence=precedence,
407 store_plaintext=store_plaintext,
408 history_length=history_length,
409 min_pwd_length=min_pwd_length,
410 min_pwd_age=min_pwd_age, max_pwd_age=max_pwd_age,
411 lockout_duration=account_lockout_duration,
412 lockout_threshold=account_lockout_threshold,
413 reset_lockout_after=reset_account_lockout_after)
415 # create the new PSO
416 try:
417 samdb.add(m)
418 self.message("PSO successfully created: %s" % pso_dn)
419 # display the new PSO's settings
420 show_pso_by_dn(self.outf, samdb, pso_dn, show_applies_to=False)
421 except ldb.LdbError as e:
422 (num, msg) = e.args
423 if num == ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS:
424 raise CommandError("Administrator permissions are needed "
425 "to create a PSO.")
426 else:
427 raise CommandError("Failed to create PSO '%s': %s" % (pso_dn,
428 msg))
431 class cmd_domain_pwdsettings_pso_set(Command):
432 """Modifies a Password Settings Object (PSO)."""
434 synopsis = "%prog <psoname> [options]"
436 takes_optiongroups = {
437 "sambaopts": options.SambaOptions,
438 "versionopts": options.VersionOptions,
439 "credopts": options.CredentialsOptions,
442 takes_options = pwd_settings_options + [
443 Option("--precedence", type=int,
444 help=("This PSO's precedence relative to other PSOs. "
445 "Lower precedence is better (<integer>).")),
446 Option("-H", "--URL", help="LDB URL for database or target server",
447 type=str, metavar="URL", dest="H"),
449 takes_args = ["psoname"]
451 def run(self, psoname, H=None, precedence=None, min_pwd_age=None,
452 max_pwd_age=None, complexity=None, store_plaintext=None,
453 history_length=None, min_pwd_length=None,
454 account_lockout_duration=None, account_lockout_threshold=None,
455 reset_account_lockout_after=None, credopts=None, sambaopts=None,
456 versionopts=None):
457 lp = sambaopts.get_loadparm()
458 creds = credopts.get_credentials(lp)
460 samdb = SamDB(url=H, session_info=system_session(),
461 credentials=creds, lp=lp)
463 # sanity-check the PSO exists
464 pso_dn = "CN=%s,%s" % (psoname, pso_container(samdb))
465 check_pso_valid(samdb, pso_dn, psoname)
467 # we expect the user to specify at least one password-policy setting
468 num_pwd_args = num_options_in_args(pwd_settings_options, self.raw_argv)
469 if num_pwd_args == 0 and precedence is None:
470 raise CommandError("Please specify at least one password policy "
471 "setting. Try --help")
473 if min_pwd_age is not None or max_pwd_age is not None:
474 # if we're modifying either the max or min pwd-age, check the max
475 # is always larger. We may have to fetch the PSO's setting to
476 # verify this
477 res = samdb.search(pso_dn, scope=ldb.SCOPE_BASE,
478 attrs=['msDS-MinimumPasswordAge',
479 'msDS-MaximumPasswordAge'])
480 if min_pwd_age is None:
481 min_pwd_ticks = res[0]['msDS-MinimumPasswordAge'][0]
482 min_pwd_age = timestamp_to_days(min_pwd_ticks)
484 if max_pwd_age is None:
485 max_pwd_ticks = res[0]['msDS-MaximumPasswordAge'][0]
486 max_pwd_age = timestamp_to_days(max_pwd_ticks)
488 check_pso_constraints(max_pwd_age=max_pwd_age, min_pwd_age=min_pwd_age,
489 history_length=history_length,
490 min_pwd_length=min_pwd_length)
492 # pack the settings into an LDB message
493 m = make_pso_ldb_msg(self.outf, samdb, pso_dn, create=False,
494 complexity=complexity, precedence=precedence,
495 store_plaintext=store_plaintext,
496 history_length=history_length,
497 min_pwd_length=min_pwd_length,
498 min_pwd_age=min_pwd_age, max_pwd_age=max_pwd_age,
499 lockout_duration=account_lockout_duration,
500 lockout_threshold=account_lockout_threshold,
501 reset_lockout_after=reset_account_lockout_after)
503 # update the PSO
504 try:
505 samdb.modify(m)
506 self.message("Successfully updated PSO: %s" % pso_dn)
507 # display the new PSO's settings
508 show_pso_by_dn(self.outf, samdb, pso_dn, show_applies_to=False)
509 except ldb.LdbError as e:
510 (num, msg) = e.args
511 raise CommandError("Failed to update PSO '%s': %s" % (pso_dn, msg))
514 class cmd_domain_pwdsettings_pso_delete(Command):
515 """Deletes a Password Settings Object (PSO)."""
517 synopsis = "%prog <psoname> [options]"
519 takes_optiongroups = {
520 "sambaopts": options.SambaOptions,
521 "versionopts": options.VersionOptions,
522 "credopts": options.CredentialsOptions,
525 takes_options = [
526 Option("-H", "--URL", help="LDB URL for database or target server",
527 metavar="URL", dest="H", type=str)
529 takes_args = ["psoname"]
531 def run(self, psoname, H=None, credopts=None, sambaopts=None,
532 versionopts=None):
533 lp = sambaopts.get_loadparm()
534 creds = credopts.get_credentials(lp)
536 samdb = SamDB(url=H, session_info=system_session(),
537 credentials=creds, lp=lp)
539 pso_dn = "CN=%s,%s" % (psoname, pso_container(samdb))
540 # sanity-check the PSO exists
541 check_pso_valid(samdb, pso_dn, psoname)
543 samdb.delete(pso_dn)
544 self.message("Deleted PSO %s" % psoname)
547 def pso_key(a):
548 a_precedence = int(a['msDS-PasswordSettingsPrecedence'][0])
549 return a_precedence
552 class cmd_domain_pwdsettings_pso_list(Command):
553 """Lists all Password Settings Objects (PSOs)."""
555 synopsis = "%prog [options]"
557 takes_optiongroups = {
558 "sambaopts": options.SambaOptions,
559 "versionopts": options.VersionOptions,
560 "credopts": options.CredentialsOptions,
563 takes_options = [
564 Option("-H", "--URL", help="LDB URL for database or target server",
565 metavar="URL", dest="H", type=str)
568 def run(self, H=None, credopts=None, sambaopts=None, versionopts=None):
569 lp = sambaopts.get_loadparm()
570 creds = credopts.get_credentials(lp)
572 samdb = SamDB(url=H, session_info=system_session(),
573 credentials=creds, lp=lp)
575 res = samdb.search(pso_container(samdb), scope=ldb.SCOPE_SUBTREE,
576 attrs=['name', 'msDS-PasswordSettingsPrecedence'],
577 expression="(objectClass=msDS-PasswordSettings)")
579 # an unprivileged search against Windows returns nothing here. On Samba
580 # we get the PSO names, but not their attributes
581 if len(res) == 0 or 'msDS-PasswordSettingsPrecedence' not in res[0]:
582 self.outf.write("No PSOs are present, or you don't have permission"
583 " to view them.\n")
584 return
586 # sort the PSOs so they're displayed in order of precedence
587 pso_list = sorted(res, key=pso_key)
589 self.outf.write("Precedence | PSO name\n")
590 self.outf.write("--------------------------------------------------\n")
592 for pso in pso_list:
593 precedence = pso['msDS-PasswordSettingsPrecedence']
594 self.outf.write("%-10s | %s\n" % (precedence, pso['name']))
597 class cmd_domain_pwdsettings_pso_show(Command):
598 """Display a Password Settings Object's details."""
600 synopsis = "%prog <psoname> [options]"
602 takes_optiongroups = {
603 "sambaopts": options.SambaOptions,
604 "versionopts": options.VersionOptions,
605 "credopts": options.CredentialsOptions,
608 takes_options = [
609 Option("-H", "--URL", help="LDB URL for database or target server",
610 metavar="URL", dest="H", type=str)
612 takes_args = ["psoname"]
614 def run(self, psoname, H=None, credopts=None, sambaopts=None,
615 versionopts=None):
616 lp = sambaopts.get_loadparm()
617 creds = credopts.get_credentials(lp)
619 samdb = SamDB(url=H, session_info=system_session(),
620 credentials=creds, lp=lp)
622 pso_dn = "CN=%s,%s" % (psoname, pso_container(samdb))
623 check_pso_valid(samdb, pso_dn, psoname)
624 show_pso_by_dn(self.outf, samdb, pso_dn)
627 class cmd_domain_pwdsettings_pso_show_user(Command):
628 """Displays the Password Settings that apply to a user."""
630 synopsis = "%prog <username> [options]"
632 takes_optiongroups = {
633 "sambaopts": options.SambaOptions,
634 "versionopts": options.VersionOptions,
635 "credopts": options.CredentialsOptions,
638 takes_options = [
639 Option("-H", "--URL", help="LDB URL for database or target server",
640 metavar="URL", dest="H", type=str)
642 takes_args = ["username"]
644 def run(self, username, H=None, credopts=None, sambaopts=None,
645 versionopts=None):
646 lp = sambaopts.get_loadparm()
647 creds = credopts.get_credentials(lp)
649 samdb = SamDB(url=H, session_info=system_session(),
650 credentials=creds, lp=lp)
652 show_pso_for_user(self.outf, samdb, username)
655 class cmd_domain_pwdsettings_pso_apply(Command):
656 """Applies a PSO's password policy to a user or group.
658 When a PSO is applied to a group, it will apply to all users (and groups)
659 that are members of that group. If a PSO applies directly to a user, it
660 will override any group membership PSOs for that user.
662 When multiple PSOs apply to a user, either directly or through group
663 membership, the PSO with the lowest precedence will take effect.
666 synopsis = "%prog <psoname> <user-or-group-name> [options]"
668 takes_optiongroups = {
669 "sambaopts": options.SambaOptions,
670 "versionopts": options.VersionOptions,
671 "credopts": options.CredentialsOptions,
674 takes_options = [
675 Option("-H", "--URL", help="LDB URL for database or target server",
676 metavar="URL", dest="H", type=str)
678 takes_args = ["psoname", "user_or_group"]
680 def run(self, psoname, user_or_group, H=None, credopts=None,
681 sambaopts=None, versionopts=None):
682 lp = sambaopts.get_loadparm()
683 creds = credopts.get_credentials(lp)
685 samdb = SamDB(url=H, session_info=system_session(),
686 credentials=creds, lp=lp)
688 pso_dn = "CN=%s,%s" % (psoname, pso_container(samdb))
689 # sanity-check the PSO exists
690 check_pso_valid(samdb, pso_dn, psoname)
692 # lookup the user/group by account-name to gets its DN
693 search_filter = "(sAMAccountName=%s)" % user_or_group
694 res = samdb.search(samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE,
695 expression=search_filter)
697 if len(res) == 0:
698 raise CommandError("The specified user or group '%s' was not found"
699 % user_or_group)
701 # modify the PSO to apply to the user/group specified
702 target_dn = str(res[0].dn)
703 m = ldb.Message()
704 m.dn = ldb.Dn(samdb, pso_dn)
705 m["msDS-PSOAppliesTo"] = ldb.MessageElement(target_dn,
706 ldb.FLAG_MOD_ADD,
707 "msDS-PSOAppliesTo")
708 try:
709 samdb.modify(m)
710 except ldb.LdbError as e:
711 (num, msg) = e.args
712 # most likely error - PSO already applies to that user/group
713 if num == ldb.ERR_ATTRIBUTE_OR_VALUE_EXISTS:
714 raise CommandError("PSO '%s' already applies to '%s'"
715 % (psoname, user_or_group))
716 else:
717 raise CommandError("Failed to update PSO '%s': %s" % (psoname,
718 msg))
720 self.message("PSO '%s' applied to '%s'" % (psoname, user_or_group))
723 class cmd_domain_pwdsettings_pso_unapply(Command):
724 """Updates a PSO to no longer apply to a user or group."""
726 synopsis = "%prog <psoname> <user-or-group-name> [options]"
728 takes_optiongroups = {
729 "sambaopts": options.SambaOptions,
730 "versionopts": options.VersionOptions,
731 "credopts": options.CredentialsOptions,
734 takes_options = [
735 Option("-H", "--URL", help="LDB URL for database or target server",
736 metavar="URL", dest="H", type=str),
738 takes_args = ["psoname", "user_or_group"]
740 def run(self, psoname, user_or_group, H=None, credopts=None,
741 sambaopts=None, versionopts=None):
742 lp = sambaopts.get_loadparm()
743 creds = credopts.get_credentials(lp)
745 samdb = SamDB(url=H, session_info=system_session(),
746 credentials=creds, lp=lp)
748 pso_dn = "CN=%s,%s" % (psoname, pso_container(samdb))
749 # sanity-check the PSO exists
750 check_pso_valid(samdb, pso_dn, psoname)
752 # lookup the user/group by account-name to gets its DN
753 search_filter = "(sAMAccountName=%s)" % user_or_group
754 res = samdb.search(samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE,
755 expression=search_filter)
757 if len(res) == 0:
758 raise CommandError("The specified user or group '%s' was not found"
759 % user_or_group)
761 # modify the PSO to apply to the user/group specified
762 target_dn = str(res[0].dn)
763 m = ldb.Message()
764 m.dn = ldb.Dn(samdb, pso_dn)
765 m["msDS-PSOAppliesTo"] = ldb.MessageElement(target_dn,
766 ldb.FLAG_MOD_DELETE,
767 "msDS-PSOAppliesTo")
768 try:
769 samdb.modify(m)
770 except ldb.LdbError as e:
771 (num, msg) = e.args
772 # most likely error - PSO doesn't apply to that user/group
773 if num == ldb.ERR_NO_SUCH_ATTRIBUTE:
774 raise CommandError("PSO '%s' doesn't apply to '%s'"
775 % (psoname, user_or_group))
776 else:
777 raise CommandError("Failed to update PSO '%s': %s" % (psoname,
778 msg))
779 self.message("PSO '%s' no longer applies to '%s'" % (psoname,
780 user_or_group))
783 class cmd_domain_passwordsettings_pso(SuperCommand):
784 """Manage fine-grained Password Settings Objects (PSOs)."""
786 subcommands = {}
787 subcommands["apply"] = cmd_domain_pwdsettings_pso_apply()
788 subcommands["create"] = cmd_domain_pwdsettings_pso_create()
789 subcommands["delete"] = cmd_domain_pwdsettings_pso_delete()
790 subcommands["list"] = cmd_domain_pwdsettings_pso_list()
791 subcommands["set"] = cmd_domain_pwdsettings_pso_set()
792 subcommands["show"] = cmd_domain_pwdsettings_pso_show()
793 subcommands["show-user"] = cmd_domain_pwdsettings_pso_show_user()
794 subcommands["unapply"] = cmd_domain_pwdsettings_pso_unapply()