Add support for bind grace limiting per https://datatracker.ietf.org/doc/html/draft-behera-ldap-password-policy-06 389-ds provides for alternative naming than the draft, using those instead: passwordGraceUserTime for pwdGraceUserTime and passwordGraceLimit for pwdGraceLoginLimit. passwordGraceLimit is a policy variable that an administrator sets to determine the maximum number of LDAP binds allowed when a password is marked as expired. This is suported for both the global and per-group password policies. passwordGraceUserTime is a count per-user of the number of binds. When the passwordGraceUserTime exceeds the passwordGraceLimit then all subsequent binds will be denied and an administrator will need to reset the user password. If passwordGraceLimit is less than 0 then grace limiting is disabled and unlimited binds are allowed. Grace login limitations only apply to entries with the objectclass posixAccount or simplesecurityobject in order to limit this to IPA users and system accounts. Some basic support for the LDAP ppolicy control is enabled such that if the ppolicy control is in the bind request then the number of remaining grace binds will be returned with the request. The passwordGraceUserTime attribute is reset to 0 upon a password reset. user-status has been extended to display the number of grace binds which is stored centrally and not per-server. Note that passwordGraceUserTime is an operational attribute. https://pagure.io/freeipa/issue/1539 Signed-off-by: Rob Crittenden <rcritten@redhat.com> Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
354 lines
14 KiB
Python
354 lines
14 KiB
Python
#
|
|
# Copyright (C) 2020 FreeIPA Contributors see COPYING for license
|
|
#
|
|
"""Misc test for 'ipa' CLI regressions
|
|
"""
|
|
from __future__ import absolute_import
|
|
|
|
import pytest
|
|
|
|
from ipatests.test_integration.base import IntegrationTest
|
|
|
|
from ipatests.pytest_ipa.integration import tasks
|
|
|
|
USER = 'tuser'
|
|
PASSWORD = 'Secret123'
|
|
POLICY = 'test'
|
|
|
|
|
|
class TestPWPolicy(IntegrationTest):
|
|
"""
|
|
Test password policy in action.
|
|
"""
|
|
num_replicas = 1
|
|
|
|
topology = 'line'
|
|
|
|
@classmethod
|
|
def install(cls, mh):
|
|
super(TestPWPolicy, cls).install(mh)
|
|
|
|
tasks.kinit_admin(cls.master)
|
|
cls.master.run_command(['ipa', 'user-add', USER,
|
|
'--first', 'Test',
|
|
'--last', 'User'])
|
|
cls.master.run_command(['ipa', 'group-add', POLICY])
|
|
cls.master.run_command(['ipa', 'group-add-member', POLICY,
|
|
'--users', USER])
|
|
cls.master.run_command(['ipa', 'pwpolicy-add', POLICY,
|
|
'--priority', '1'])
|
|
cls.master.run_command(['ipa', 'passwd', USER],
|
|
stdin_text='{password}\n{password}\n'.format(
|
|
password=PASSWORD
|
|
))
|
|
|
|
def kinit_as_user(self, host, old_password, new_password, user=USER,
|
|
raiseonerr=True):
|
|
"""kinit to an account with an expired password"""
|
|
return host.run_command(
|
|
['kinit', user],
|
|
raiseonerr=raiseonerr,
|
|
stdin_text='{old}\n{new}\n{new}\n'.format(
|
|
old=old_password, new=new_password
|
|
),
|
|
)
|
|
|
|
def reset_password(self, host, user=USER, password=PASSWORD):
|
|
tasks.kinit_admin(host)
|
|
host.run_command(
|
|
['ipa', 'passwd', user],
|
|
stdin_text='{password}\n{password}\n'.format(password=password),
|
|
)
|
|
|
|
def set_pwpolicy(self, minlength=None, maxrepeat=None, maxsequence=None,
|
|
dictcheck=None, usercheck=None, minclasses=None):
|
|
tasks.kinit_admin(self.master)
|
|
args = ["ipa", "pwpolicy-mod", POLICY]
|
|
if minlength is not None:
|
|
args.append("--minlength={}".format(minlength))
|
|
if maxrepeat is not None:
|
|
args.append("--maxrepeat={}".format(maxrepeat))
|
|
if maxsequence is not None:
|
|
args.append("--maxsequence={}".format(maxsequence))
|
|
if dictcheck is not None:
|
|
args.append("--dictcheck={}".format(dictcheck))
|
|
if usercheck is not None:
|
|
args.append("--usercheck={}".format(usercheck))
|
|
if minclasses is not None:
|
|
args.append("--minclasses={}".format(minclasses))
|
|
self.master.run_command(args)
|
|
|
|
self.reset_password(self.master)
|
|
|
|
def clean_pwpolicy(self):
|
|
"""Set all policy values we care about to zero/false"""
|
|
self.master.run_command(
|
|
["ipa", "pwpolicy-mod", POLICY,
|
|
"--maxrepeat", "0",
|
|
"--maxsequence", "0",
|
|
"--usercheck", "false",
|
|
"--dictcheck" ,"false",
|
|
"--minlife", "0",
|
|
"--minlength", "0",
|
|
"--minclasses", "0",],
|
|
)
|
|
|
|
@pytest.fixture
|
|
def reset_pwpolicy(self):
|
|
"""Fixture to ensure policy is reset between tests"""
|
|
yield
|
|
tasks.kinit_admin(self.master)
|
|
self.clean_pwpolicy()
|
|
|
|
def test_maxrepeat(self, reset_pwpolicy):
|
|
self.set_pwpolicy(maxrepeat=2)
|
|
# good passwords
|
|
for password in ('Secret123', 'Password'):
|
|
self.reset_password(self.master)
|
|
self.kinit_as_user(self.master, PASSWORD, password)
|
|
self.reset_password(self.master)
|
|
tasks.ldappasswd_user_change(USER, PASSWORD, password, self.master)
|
|
|
|
self.reset_password(self.master)
|
|
|
|
# bad passwords
|
|
for password in ('Secret1111', 'passsword'):
|
|
result = self.kinit_as_user(self.master, PASSWORD, password,
|
|
raiseonerr=False)
|
|
assert result.returncode == 1
|
|
result = tasks.ldappasswd_user_change(USER, PASSWORD, password,
|
|
self.master,
|
|
raiseonerr=False)
|
|
assert result.returncode == 1
|
|
assert 'Password has too many consecutive characters' in \
|
|
result.stdout_text
|
|
|
|
def test_maxsequence(self, reset_pwpolicy):
|
|
self.set_pwpolicy(maxsequence=3)
|
|
# good passwords
|
|
for password in ('Password123', 'Passwordabc'):
|
|
self.reset_password(self.master)
|
|
self.kinit_as_user(self.master, PASSWORD, password)
|
|
self.reset_password(self.master)
|
|
tasks.ldappasswd_user_change(USER, PASSWORD, password, self.master)
|
|
|
|
self.reset_password(self.master)
|
|
|
|
# bad passwords
|
|
for password in ('Password1234', 'Passwordabcde'):
|
|
result = self.kinit_as_user(self.master, PASSWORD, password,
|
|
raiseonerr=False)
|
|
assert result.returncode == 1
|
|
result = tasks.ldappasswd_user_change(USER, PASSWORD, password,
|
|
self.master,
|
|
raiseonerr=False)
|
|
assert result.returncode == 1
|
|
assert 'Password contains a monotonic sequence' in \
|
|
result.stdout_text
|
|
|
|
def test_usercheck(self, reset_pwpolicy):
|
|
self.set_pwpolicy(usercheck=True)
|
|
for password in ('tuserpass', 'passoftuser'):
|
|
result = self.kinit_as_user(self.master, PASSWORD, password,
|
|
raiseonerr=False)
|
|
assert result.returncode == 1
|
|
result = tasks.ldappasswd_user_change(USER, PASSWORD, password,
|
|
self.master,
|
|
raiseonerr=False)
|
|
assert result.returncode == 1
|
|
assert 'Password contains username' in \
|
|
result.stdout_text
|
|
|
|
# test with valid password
|
|
self.kinit_as_user(self.master, PASSWORD, 'bamOncyftAv0')
|
|
|
|
def test_dictcheck(self, reset_pwpolicy):
|
|
self.set_pwpolicy(dictcheck=True)
|
|
for password in ('password', 'bookends', 'BaLtim0re'):
|
|
result = self.kinit_as_user(self.master, PASSWORD, password,
|
|
raiseonerr=False)
|
|
assert result.returncode == 1
|
|
result = tasks.ldappasswd_user_change(USER, PASSWORD, password,
|
|
self.master,
|
|
raiseonerr=False)
|
|
assert result.returncode == 1
|
|
assert 'Password is based on a dictionary word' in \
|
|
result.stdout_text
|
|
|
|
# test with valid password
|
|
self.kinit_as_user(self.master, PASSWORD, 'bamOncyftAv0')
|
|
|
|
def test_minclasses(self, reset_pwpolicy):
|
|
self.set_pwpolicy(minclasses=2)
|
|
for password in ('password', 'bookends'):
|
|
result = self.kinit_as_user(self.master, PASSWORD, password,
|
|
raiseonerr=False)
|
|
assert result.returncode == 1
|
|
assert 'Password does not contain enough character' in \
|
|
result.stdout_text
|
|
result = tasks.ldappasswd_user_change(USER, PASSWORD, password,
|
|
self.master,
|
|
raiseonerr=False)
|
|
assert result.returncode == 1
|
|
assert 'Password is too simple' in \
|
|
result.stdout_text
|
|
|
|
# test with valid password
|
|
for valid in ('Password', 'password1', 'password!'):
|
|
self.kinit_as_user(self.master, PASSWORD, valid)
|
|
self.reset_password(self.master)
|
|
|
|
self.set_pwpolicy(minclasses=3)
|
|
for password in ('password1', 'Bookends'):
|
|
result = self.kinit_as_user(self.master, PASSWORD, password,
|
|
raiseonerr=False)
|
|
assert result.returncode == 1
|
|
assert 'Password does not contain enough character' in \
|
|
result.stdout_text
|
|
result = tasks.ldappasswd_user_change(USER, PASSWORD, password,
|
|
self.master,
|
|
raiseonerr=False)
|
|
assert result.returncode == 1
|
|
assert 'Password is too simple' in \
|
|
result.stdout_text
|
|
|
|
# test with valid password
|
|
for valid in ('Passw0rd', 'password1!', 'Password!'):
|
|
self.kinit_as_user(self.master, PASSWORD, valid)
|
|
self.reset_password(self.master)
|
|
|
|
def test_minlength_mod(self, reset_pwpolicy):
|
|
"""Test that the pwpolicy minlength overrides our policy
|
|
"""
|
|
|
|
# With a minlength of 4 all settings of pwq should fail
|
|
self.master.run_command(
|
|
["ipa", "pwpolicy-mod", POLICY,
|
|
"--minlength", "4",]
|
|
)
|
|
for values in (('--maxrepeat', '4'),
|
|
('--maxsequence', '4'),
|
|
('--dictcheck', 'true'),
|
|
('--usercheck', 'true')):
|
|
args = ["ipa", "pwpolicy-mod", POLICY]
|
|
args.extend(values)
|
|
result = self.master.run_command(args, raiseonerr=False)
|
|
assert result.returncode != 0
|
|
assert 'minlength' in result.stderr_text
|
|
|
|
# With any pwq value set, setting minlife < 6 should fail
|
|
for values in (('--maxrepeat', '4'),
|
|
('--maxsequence', '4'),
|
|
('--dictcheck', 'true'),
|
|
('--usercheck', 'true')):
|
|
self.clean_pwpolicy()
|
|
args = ["ipa", "pwpolicy-mod", POLICY]
|
|
args.extend(values)
|
|
self.master.run_command(args)
|
|
result = self.master.run_command(
|
|
["ipa", "pwpolicy-mod", POLICY,
|
|
"--minlength", "4",], raiseonerr=False
|
|
)
|
|
assert result.returncode != 0
|
|
assert 'minlength' in result.stderr_text
|
|
|
|
def test_minlength_add(self):
|
|
"""Test that adding a new policy with minlength is caught.
|
|
"""
|
|
result = self.master.run_command(
|
|
["ipa", "pwpolicy-add", "test_add",
|
|
"--maxrepeat", "4", "--minlength", "4", "--priority", "2"],
|
|
raiseonerr=False
|
|
)
|
|
assert result.returncode != 0
|
|
assert 'minlength' in result.stderr_text
|
|
|
|
def test_graceperiod_expired(self):
|
|
"""Test the LDAP bind grace period"""
|
|
str(self.master.domain.basedn)
|
|
dn = "uid={user},cn=users,cn=accounts,{base_dn}".format(
|
|
user=USER, base_dn=str(self.master.domain.basedn))
|
|
|
|
self.master.run_command(
|
|
["ipa", "pwpolicy-mod", POLICY, "--gracelimit", "3", ],
|
|
)
|
|
|
|
# Resetting the password will mark it as expired
|
|
self.reset_password(self.master)
|
|
|
|
for i in range(2, -1, -1):
|
|
result = self.master.run_command(
|
|
["ldapsearch", "-e", "ppolicy", "-D", dn,
|
|
"-w", PASSWORD, "-b", dn], raiseonerr=False
|
|
)
|
|
# We're in grace, this will succeed
|
|
assert result.returncode == 0
|
|
|
|
# verify that we get the expected ppolicy output
|
|
assert 'Password expired, {} grace logins remain'.format(i) \
|
|
in result.stderr_text
|
|
|
|
# Now grace is done and binds should fail.
|
|
result = self.master.run_command(
|
|
["ldapsearch", "-e", "ppolicy", "-D", dn,
|
|
"-w", PASSWORD, "-b", dn], raiseonerr=False
|
|
)
|
|
assert result.returncode == 49
|
|
|
|
assert 'Password is expired' in result.stderr_text
|
|
assert 'Password expired, 0 grace logins remain' in result.stderr_text
|
|
|
|
# Test that resetting the password resets the grace counter
|
|
self.reset_password(self.master)
|
|
result = tasks.ldapsearch_dm(
|
|
self.master, dn, ['passwordgraceusertime',],
|
|
)
|
|
|
|
assert 'passwordgraceusertime: 0' in result.stdout_text.lower()
|
|
|
|
def test_graceperiod_not_replicated(self):
|
|
"""Test that the grace period is reset on password reset"""
|
|
str(self.master.domain.basedn)
|
|
dn = "uid={user},cn=users,cn=accounts,{base_dn}".format(
|
|
user=USER, base_dn=str(self.master.domain.basedn))
|
|
|
|
self.master.run_command(
|
|
["ipa", "pwpolicy-mod", POLICY, "--gracelimit", "3", ],
|
|
)
|
|
|
|
# Resetting the password will mark it as expired
|
|
self.reset_password(self.master)
|
|
|
|
# Generate some logins but don't exceed the limit
|
|
for _i in range(2, -1, -1):
|
|
result = self.master.run_command(
|
|
["ldapsearch", "-e", "ppolicy", "-D", dn,
|
|
"-w", PASSWORD, "-b", dn], raiseonerr=False
|
|
)
|
|
|
|
# Verify that passwordgraceusertime is not replicated
|
|
result = tasks.ldapsearch_dm(
|
|
self.master, dn, ['passwordgraceusertime',],
|
|
)
|
|
assert 'passwordgraceusertime: 2' in result.stdout_text.lower()
|
|
|
|
result = tasks.ldapsearch_dm(
|
|
self.replicas[0], dn, ['passwordgraceusertime',],
|
|
)
|
|
assert 'passwordgraceusertime: 0' in result.stdout_text.lower()
|
|
|
|
self.reset_password(self.master)
|
|
|
|
# Resetting the password should reset passwordgraceusertime
|
|
result = tasks.ldapsearch_dm(
|
|
self.master, dn, ['passwordgraceusertime',],
|
|
)
|
|
assert 'passwordgraceusertime: 0' in result.stdout_text.lower()
|
|
self.reset_password(self.master)
|
|
|
|
result = tasks.ldapsearch_dm(
|
|
self.replicas[0], dn, ['passwordgraceusertime',],
|
|
)
|
|
assert 'passwordgraceusertime: 0' in result.stdout_text.lower()
|
|
self.reset_password(self.master)
|