Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

roles, adding default domain password policy management #139

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
16 changes: 16 additions & 0 deletions sssd_test_framework/misc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,22 @@ def attrs_to_hash(attrs: dict[str, Any]) -> str | None:
RetType = TypeVar("RetType")


def seconds_to_timespan(seconds: int) -> str:
"""
Convert seconds to powershell timespan format, 'Days:Hours:Minutes:Seconds:Fractions'.

:param seconds: Seconds.
:type seconds: int
:return: Time in timespan format.
:rtype: str
"""
m, s = divmod(seconds, 60)
h, m = divmod(m, 60)
d, h = divmod(h, 24)

return f"{d:02d}:{h:02d}:{m:02d}:{s:02d}:00"


def retry(
max_retries: int = 5,
delay: float = 1,
Expand Down
127 changes: 126 additions & 1 deletion sssd_test_framework/roles/ad.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
from pytest_mh.conn import ProcessResult

from ..hosts.ad import ADHost
from ..misc import attrs_include_value, attrs_parse, attrs_to_hash
from ..misc import attrs_include_value, attrs_parse, attrs_to_hash, seconds_to_timespan
from .base import BaseObject, BaseWindowsRole, DeleteAttribute
from .generic import GenericPasswordPolicy
from .ldap import LDAPNetgroupMember
from .nfs import NFSExport

Expand All @@ -24,6 +25,7 @@
"ADComputer",
"ADSudoRule",
"ADUser",
"ADPasswordPolicy",
"GPO",
]

Expand Down Expand Up @@ -138,6 +140,30 @@ def fqn(self, name: str) -> str:
"""
return f"{name}@{self.domain}"

@property
def password(self) -> ADPasswordPolicy:
"""
Domain password policy management.

.. code-block:: python
:caption: Example usage

@pytest.mark.topology(KnownTopology.AD)
def test_example(client: Client, ad: AD):
# Enable password complexity
ad.password.complexity(enable=True)

# Set 3 login attempts and 30 lockout duration
ad.password.lockout(attempts=3, duration=30)

# Set password length requirement to 12 characters
ad.password.requirement(length=12)

# Set password max age to 30 seconds
ad.password.age(maximum=30)
"""
return ADPasswordPolicy(self)

def user(self, name: str, basedn: ADObject | str | None = "cn=users") -> ADUser:
"""
Get user object.
Expand Down Expand Up @@ -911,6 +937,18 @@ def expire(self, expiration: str = "19700101000000") -> ADUser:

return self

@property
def password_change_at_logon(self) -> ADUser:
"""
Force user to change password next logon.

:return: Self.
:rtype: ADUser
"""
self.role.host.conn.run(f"Set-ADUser -Identity {self.name} -ChangePasswordAtLogon:$true")

return self

def passkey_add(self, passkey_mapping: str) -> ADUser:
"""
Add passkey mapping to the user.
Expand Down Expand Up @@ -2009,4 +2047,91 @@ def policy(self, logon_rights: dict[str, list[ADObject]], cfg: dict[str, Any] |
return self


class ADPasswordPolicy(GenericPasswordPolicy):
"""
Password policy management.
"""

def __init__(self, role: AD):
"""
:param role: AD host object.
:type role: ADHost
"""
super().__init__(role)

def complexity(self, enable: bool) -> ADPasswordPolicy:
"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure whether this documentation is needed if it is already present in the superclass.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to change this back to a @property , so it's not instantiated with every call, I forgot about that and I will put usage examples in that docstring.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making this function a property will not allow you to return a ADPasswordPolicy object (self) in order to chain method invocations. I have nothing against it, but it doesn't align with the rest of your code. Do as you want.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah that explains something I didn't quite understand. I made this decision change because it wouldn't automatically instantiate the object, but I do want the other behavior so I will change this back.

Enable or disable password complexity.

:param enable: Enable or disable password complexity.
:type enable: bool
:return: ADPasswordPolicy object.
:rtype: ADPasswordPolicy
"""
args: CLIBuilderArgs = {
"Identity": (self.cli.option.VALUE, self.role.domain),
"Complexity": (self.cli.option.SWITCH, enable),
}
self.role.host.conn.run(self.cli.command("Set-ADDefaultDomainPasswordPolicy", args))

return self

def lockout(self, duration: int, attempts: int) -> ADPasswordPolicy:
"""
Set lockout duration and login attempts.

:param duration: Duration of lockout in seconds.
:type duration: int
:param attempts: Number of login attempts.
:type attempts: int
:return: ADPasswordPolicy object.
:rtype: ADPasswordPolicy
"""
args: CLIBuilderArgs = {
"Identity": (self.cli.option.VALUE, self.role.domain),
"LockoutDuration": (self.cli.option.VALUE, seconds_to_timespan(duration)),
"LockoutThreshold": (self.cli.option.VALUE, str(attempts)),
}
self.role.host.conn.run(self.cli.command("Set-ADDefaultDomainPasswordPolicy", args))

return self

def age(self, minimum: int, maximum: int) -> ADPasswordPolicy:
"""
Set maximum and minimum password age.

:param minimum: Minimum password age in seconds.
:type minimum: int
:param maximum: Maximum password age in seconds.
:type maximum: int
:return: ADPasswordPolicy object.
:rtype: ADPasswordPolicy
"""
args: CLIBuilderArgs = {
"Identity": (self.cli.option.VALUE, self.role.domain),
"MinPasswordAge": (self.cli.option.VALUE, seconds_to_timespan(minimum)),
"MaxPasswordAge": (self.cli.option.VALUE, seconds_to_timespan(maximum)),
}
self.role.host.conn.run(self.cli.command("Set-ADDefaultDomainPasswordPolicy", args))

return self

def requirements(self, length: int) -> ADPasswordPolicy:
"""
Set password requirements, like length.

:param length: Required password character count.
:type length: int
:return: ADPasswordPolicy object.
:rtype: ADPasswordPolicy
"""
args: CLIBuilderArgs = {
"Identity": (self.cli.option.VALUE, self.role.domain),
"MinPasswordLength": (self.cli.option.VALUE, str(length)),
}
self.role.host.conn.run(self.cli.command("Set-ADDefaultDomainPasswordPolicy", args))

return self


ADNetgroupMember: TypeAlias = LDAPNetgroupMember[ADUser, ADNetgroup]
95 changes: 95 additions & 0 deletions sssd_test_framework/roles/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"GenericProvider",
"GenericADProvider",
"GenericOrganizationalUnit",
"GenericPasswordPolicy",
"GenericUser",
"GenericGroup",
"GenericComputer",
Expand Down Expand Up @@ -76,6 +77,31 @@ def features(self) -> dict[str, Any]:
def firewall(self) -> Firewall:
pass

@property
@abstractmethod
def password(self) -> GenericPasswordPolicy:
"""
Domain password policy management.

.. code-block:: python
:caption: Example usage

@pytest.mark.topology(KnownTopologyGroup.Any)
def test_example(client: Client, provider: GenericProvider):
# Enable password complexity
provider.password.complexity(enable=True)

# Set 3 login attempts and 30 lockout duration
provider.password.lockout(attempts=3, duration=30)

# Set password length requirement to 12 characters
provider.password.requirement(length=12)

# Set password max age to 30 seconds
provider.password.age(maximum=30)
"""
pass

@abstractmethod
def user(self, name: str) -> GenericUser:
"""
Expand Down Expand Up @@ -532,6 +558,17 @@ def expire(self, expiration: str | None = "19700101000000") -> GenericUser:
"""
pass

@property
@abstractmethod
def password_change_at_logon(self) -> GenericUser:
"""
Force user to change password next logon.

:return: Self.
:rtype: GenericUser
"""
pass

@abstractmethod
def delete(self) -> None:
"""
Expand Down Expand Up @@ -1275,3 +1312,61 @@ def policy(self, logon_rights: dict[str, list[Any]], cfg: dict[str, Any] | None
:rtype: GenericGPO
"""
pass


class GenericPasswordPolicy(ABC, BaseObject):
"""
Password policy management.
"""

@abstractmethod
def complexity(self, enable: bool) -> GenericPasswordPolicy:
"""
Enable or disable password complexity.

:param enable: Enable or disable password complexity.
:type enable: bool
:return: GenericPasswordPolicy object.
:rtype: GenericPasswordPolicy
"""
pass

@abstractmethod
def lockout(self, duration: int, attempts: int) -> GenericPasswordPolicy:
"""
Set lockout duration and login attempts.

:param duration: Duration of lockout in seconds.
:type duration: int
:param attempts: Number of login attempts.
:type attempts: int
:return: GenericPasswordPolicy object.
:rtype: GenericPasswordPolicy
"""
pass

@abstractmethod
def age(self, minimum: int, maximum: int) -> GenericPasswordPolicy:
"""
Set maximum and minimum password age.

:param minimum: Minimum password age in seconds.
:type minimum: int
:param maximum: Maximum password age in seconds.
:type maximum: int
:return: GenericPasswordPolicy object.
:rtype: GenericPasswordPolicy
"""
pass

@abstractmethod
def requirements(self, length: int) -> GenericPasswordPolicy:
aplopez marked this conversation as resolved.
Show resolved Hide resolved
"""
Set password requirements, like length.

:param length: Required password character count.
:type length: int
:return: GenericPasswordPolicy object.
:rtype: GenericPasswordPolicy
"""
pass
Loading
Loading