ipa/0020-ipatests-2FA-test-cases.patch
Florence Blanc-Renaud c8a18bb46d ipa-4.12.2-2
- Related: RHEL-59788 Rebase Samba to the latest 4.21.x release
- Fixes: RHEL-61642 Uninstall ACME separately during PKI uninstallation
- Fixes: RHEL-56963 SSSD offline causing test-adtrust-install failure
- Fixes: RHEL-56473 Include latest fixes in python3-ipatests packages
- Fixes: RHEL-48104 Default hbac rules are duplicated on remote server post ipa-migrate in prod-mode
- Fixes: RHEL-45330 [RFE] add a tool to quickly detect and fix issues with IPA ID ranges
- Fixes: RHEL-40376 SID generation task is failing when SELinux is in Enforcing mode
- Fixes: RHEL-4915 Last expired OTP token would be c

Signed-off-by: Florence Blanc-Renaud <flo@redhat.com>
2024-10-21 19:24:16 +02:00

277 lines
11 KiB
Diff

From 6ac11ae003740faf19f3c75bf542ec44f717114f Mon Sep 17 00:00:00 2001
From: Madhuri Upadhye <mupadhye@redhat.com>
Date: Tue, 23 Jul 2024 18:14:36 +0530
Subject: [PATCH] ipatests: 2FA test cases
Added following:
Added 'ssh_2fa_with_cmd' method for authentication,
as for '\n' with paramiko did not work. In a test case
need to just press `Enter` for `second factor`.
Advantage of above function is no having paramiko
dependancy.
We can run the any command in same session after
authentication of user.
Test cases:
1. Authenticate the user only with password,
just press enter at `Second factor` and check tgt after auth.
when User authentication types: otp, password
2. Authenticate the user with password and otpvalues and
check tgt of user after auth when
User authentication types: otp, password
related: https://github.com/SSSD/sssd/pull/7500
Signed-off-by: Madhuri Upadhye <mupadhye@redhat.com>
Reviewed-By: Florence Blanc-Renaud <flo@redhat.com>
---
ipatests/test_integration/test_otp.py | 192 ++++++++++++++++++++++++--
1 file changed, 181 insertions(+), 11 deletions(-)
diff --git a/ipatests/test_integration/test_otp.py b/ipatests/test_integration/test_otp.py
index 878b4fb560ba8d7768ead54b065656462545babd..0babb45897c6107bf354477dbb0d3a805a3116f5 100644
--- a/ipatests/test_integration/test_otp.py
+++ b/ipatests/test_integration/test_otp.py
@@ -5,26 +5,27 @@
"""
import base64
import logging
-import pytest
import re
-import time
+import tempfile
import textwrap
-from urllib.parse import urlparse, parse_qs
-from paramiko import AuthenticationException
+import time
+from urllib.parse import parse_qs, urlparse
+import pytest
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.twofactor.hotp import HOTP
from cryptography.hazmat.primitives.twofactor.totp import TOTP
-
-from ipatests.test_integration.base import IntegrationTest
-from ipaplatform.paths import paths
-from ipatests.pytest_ipa.integration import tasks
-from ipapython.dn import DN
-
from ldap.controls.simple import BooleanControl
+from paramiko import AuthenticationException
from ipalib import errors
+from ipaplatform.osinfo import osinfo
+from ipaplatform.paths import paths
+from ipapython.dn import DN
+from ipatests.pytest_ipa.integration import tasks
+from ipatests.test_integration.base import IntegrationTest
+from ipatests.util import xfail_context
PASSWORD = "DummyPassword123"
USER = "opttestuser"
@@ -84,6 +85,65 @@ def kinit_otp(host, user, *, password, otp, success=True):
)
+def ssh_2fa_with_cmd(host, hostname, username, password, otpvalue,
+ command="exit 0"):
+ """ ssh to user and in same session pass the command to check tgt of user
+ :param host: host to ssh
+ :param hostname: hostname to ssh
+ :param str username: The name of user
+ :param str password: password, usually the first factor
+ :param str otpvalue: generated pin of user
+ :param str command: command to execute in same session,
+ by deafult set to "exit 0"
+ :return: object class of expect command run
+ """
+ temp_conf = tempfile.NamedTemporaryFile(suffix='.exp', delete=False)
+ with open(temp_conf.name, 'w') as tfile:
+ tfile.write('proc exitmsg { msg code } {\n')
+ tfile.write('\t# Close spawned program, if we are in the prompt\n')
+ tfile.write('\tcatch close\n\n')
+ tfile.write('\t# Wait for the exit code\n')
+ tfile.write('\tlassign [wait] pid spawnid os_error_flag rc\n\n')
+ tfile.write('\tputs ""\n')
+ tfile.write('\tputs "expect result: $msg"\n')
+ tfile.write('\tputs "expect exit code: $code"\n')
+ tfile.write('\tputs "expect spawn exit code: $rc"\n')
+ tfile.write('\texit $code\n')
+ tfile.write('}\n')
+ tfile.write('set timeout 60\n')
+ tfile.write('set prompt ".*\\[#\\$>\\] $"\n')
+ tfile.write(f'set password "{password}"\n')
+ tfile.write(f'set otpvalue "{otpvalue}"\n')
+ tfile.write(f'spawn ssh -o NumberOfPasswordPrompts=1 -o '
+ f'StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'
+ f' -l {username} {hostname} {command}\n')
+ tfile.write('expect {\n')
+ tfile.write('"Enter first factor:*" {send -- "$password\r"}\n')
+ tfile.write('timeout {exitmsg "Unexpected output" 201}\n')
+ tfile.write('eof {exitmsg "Unexpected end of file" 202}\n')
+ tfile.write('}\n')
+ tfile.write('expect {\n')
+ tfile.write('"Enter second factor:*" {send -- "$otpvalue\r"}\n')
+ tfile.write('timeout {exitmsg "Unexpected output" 201}\n')
+ tfile.write('eof {exitmsg "Unexpected end of file" 202}\n')
+ tfile.write('}\n')
+ tfile.write('expect {\n')
+ tfile.write('"Authentication failure" '
+ '{exitmsg "Authentication failure" 1}\n')
+ tfile.write('eof {exitmsg "Authentication successful" 0}\n')
+ tfile.write('timeout {exitmsg "Unexpected output" 201}\n')
+ tfile.write('}\n')
+ tfile.write('expect {\n')
+ tfile.write('exitmsg "Unexpected code path" 203\n')
+ tfile.write('EOF\n')
+ tfile.write('}')
+ host.transport.put_file(temp_conf.name, '/tmp/ssh.exp')
+ tasks.clear_sssd_cache(host)
+ expect_cmd = 'expect -f /tmp/ssh.exp'
+ cmd = host.run_command(expect_cmd, raiseonerr=False)
+ return cmd
+
+
def ssh_2f(hostname, username, answers_dict, port=22, unwanted_prompt=""):
"""
:param hostname: hostname
@@ -91,6 +151,7 @@ def ssh_2f(hostname, username, answers_dict, port=22, unwanted_prompt=""):
:param answers_dict: dictionary of options with prompt_message and value.
:param port: port for ssh
"""
+
# Handler for server questions
def answer_handler(title, instructions, prompt_list):
resp = []
@@ -131,8 +192,9 @@ class TestOTPToken(IntegrationTest):
@classmethod
def install(cls, mh):
- super(TestOTPToken, cls).install(mh)
master = cls.master
+ tasks.install_packages(master, ['expect'])
+ super(TestOTPToken, cls).install(mh)
tasks.kinit_admin(master)
# create service with OTP auth indicator
@@ -398,6 +460,114 @@ class TestOTPToken(IntegrationTest):
self.master.run_command(['semanage', 'login', '-D'])
sssd_conf_backup.restore()
+ def test_2fa_only_with_password(self):
+ """Test ssh with 2FA only with the password(first factor) when
+ user-auth-type is opt and password.
+
+ Test for : https://github.com/SSSD/sssd/pull/7500
+
+ Add the IPA user and user-auth-type set to opt and password.
+ Authenticate the user only with password, just press enter
+ at `Second factor`
+ """
+ master = self.master
+ USER3 = 'sshuser3'
+ sssd_conf_backup = tasks.FileBackup(master, paths.SSSD_CONF)
+ first_prompt = 'Enter first factor:'
+ second_prompt = 'Enter second factor:'
+ add_contents = textwrap.dedent('''
+ [prompting/2fa/sshd]
+ single_prompt = False
+ first_prompt = {0}
+ second_prompt = {1}
+ ''').format(first_prompt, second_prompt)
+ set_sssd_conf(master, add_contents)
+ tasks.create_active_user(master, USER3, PASSWORD)
+ tasks.kinit_admin(master)
+ master.run_command(['ipa', 'user-mod', USER3, '--user-auth-type=otp',
+ '--user-auth-type=password'])
+ try:
+ otpuid, totp = add_otptoken(master, USER3, otptype='totp')
+ master.run_command(['ipa', 'otptoken-show', otpuid])
+ totp.generate(int(time.time())).decode('ascii')
+ otpvalue = "\n"
+ tasks.clear_sssd_cache(self.master)
+ github_ticket = "https://github.com/SSSD/sssd/pull/7500"
+ sssd_version = tasks.get_sssd_version(master)
+ rhel_fail = (
+ osinfo.id == 'rhel'
+ and sssd_version < tasks.parse_version("2.9.5")
+ )
+ fedora_fail = (
+ osinfo.id == 'fedora'
+ and sssd_version == tasks.parse_version("2.9.5")
+ )
+ with xfail_context(rhel_fail or fedora_fail, reason=github_ticket):
+ result = ssh_2fa_with_cmd(master,
+ self.master.external_hostname,
+ USER3, PASSWORD, otpvalue=otpvalue,
+ command="klist")
+ print(result.stdout_text)
+ assert ('Authentication successful') in result.stdout_text
+ assert USER3 in result.stdout_text
+ assert (f'Default principal: '
+ f'{USER3}@{self.master.domain.realm}' in
+ result.stdout_text)
+ cmd = self.master.run_command(['semanage', 'login', '-l'])
+ assert USER3 in cmd.stdout_text
+ finally:
+ master.run_command(['ipa', 'user-del', USER3])
+ self.master.run_command(['semanage', 'login', '-D'])
+ sssd_conf_backup.restore()
+
+ def test_2fa_with_otp_password(self):
+ """Test ssh with 2FA only with password and otpvalue when
+ user-auth-type is opt and password.
+
+ Test for : https://github.com/SSSD/sssd/pull/7500
+
+ Add the IPA user and user-auth-type set to opt and password.
+ Authenticate the user only with password and otpvalue.
+ """
+ master = self.master
+ USER4 = 'sshuser4'
+ sssd_conf_backup = tasks.FileBackup(master, paths.SSSD_CONF)
+ first_prompt = 'Enter first factor:'
+ second_prompt = 'Enter second factor:'
+ add_contents = textwrap.dedent('''
+ [prompting/2fa/sshd]
+ single_prompt = False
+ first_prompt = {0}
+ second_prompt = {1}
+ ''').format(first_prompt, second_prompt)
+ set_sssd_conf(master, add_contents)
+ tasks.create_active_user(master, USER4, PASSWORD)
+ tasks.kinit_admin(master)
+
+ master.run_command(['ipa', 'user-mod', USER4, '--user-auth-type=otp',
+ '--user-auth-type=password'])
+ try:
+ otpuid, totp = add_otptoken(master, USER4, otptype='totp')
+ master.run_command(['ipa', 'otptoken-show', otpuid])
+ otpvalue = totp.generate(int(time.time())).decode('ascii')
+ tasks.clear_sssd_cache(self.master)
+ result = ssh_2fa_with_cmd(master,
+ self.master.external_hostname,
+ USER4, PASSWORD, otpvalue=otpvalue,
+ command="klist")
+ print(result.stdout_text)
+ cmd = self.master.run_command(['semanage', 'login', '-l'])
+ # check the output
+ assert ('Authentication successful') in result.stdout_text
+ assert USER4 in result.stdout_text
+ assert (f'Default principal: {USER4}@'
+ f'{self.master.domain.realm}' in result.stdout_text)
+ assert USER4 in cmd.stdout_text
+ finally:
+ master.run_command(['ipa', 'user-del', USER4])
+ self.master.run_command(['semanage', 'login', '-D'])
+ sssd_conf_backup.restore()
+
@pytest.fixture
def setup_otp_nsslapd(self):
check_services = self.master.run_command(
--
2.46.2