Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sssd_test_framework/hosts/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ def features(self) -> dict[str, bool]:
echo "limited_enumeration" || :
[ -f "/usr/bin/vicc" ] && echo "virtualsmartcard" || :
[ -f "/usr/bin/umockdev-run" ] && echo "umockdev" || :
[ -f "/opt/test_venv/bin/vfido.py" ] && echo "vfido" || :
""",
log_level=ProcessLogLevel.Error,
)
Expand Down
4 changes: 4 additions & 0 deletions sssd_test_framework/misc/globals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from __future__ import annotations

test_venv = "/opt/test_venv"
test_venv_bin = f"{test_venv}/bin"
6 changes: 6 additions & 0 deletions sssd_test_framework/roles/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from ..utils.sss_override import SSSOverrideUtils
from ..utils.sssctl import SSSCTLUtils
from ..utils.sssd import SSSDUtils
from ..utils.vfido import Vfido
from .base import BaseLinuxRole

__all__ = [
Expand Down Expand Up @@ -104,6 +105,11 @@ def __init__(self, *args, **kwargs) -> None:
Managing GDM interface from SCAutolib
"""

self.vfido: Vfido = Vfido(self.host)
"""
Managing virtual passkey device and service
"""

def setup(self) -> None:
"""
Called before execution of each test.
Expand Down
70 changes: 69 additions & 1 deletion sssd_test_framework/roles/ipa.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
to_list_of_strings,
to_list_without_none,
)
from ..misc.globals import test_venv_bin
from ..roles.client import Client
from ..utils.sssctl import SSSCTLUtils
from ..utils.sssd import SSSDUtils
from .base import BaseLinuxRole, BaseObject
Expand Down Expand Up @@ -1026,7 +1028,15 @@ def passkey_add(self, passkey_mapping: str) -> IPAUser:
self._exec("add-passkey", [passkey_mapping])
return self

def passkey_add_register(
def passkey_add_register(self, **kwargs) -> str:
"""wrapper for passkey_add_register methods"""
if "virt_type" in kwargs and kwargs["virt_type"] == "vfido":
del kwargs["virt_type"]
return self.vfido_passkey_add_register(**kwargs)
else:
return self.umockdev_passkey_add_register(**kwargs)

def umockdev_passkey_add_register(
self,
*,
pin: str | int | None,
Expand Down Expand Up @@ -1103,6 +1113,64 @@ def passkey_remove(self, passkey_mapping: str) -> IPAUser:
self._exec("remove-passkey", [passkey_mapping])
return self

def vfido_passkey_add_register(
self,
*,
client: Client,
pin: str | int | None = None,
) -> str:
"""
Register user passkey when using virtual-fido
"""

if pin is None:
pin = "empty"

client.host.conn.exec(["kinit", f"{self.host.adminuser}@{self.host.realm}"], input=self.host.adminpw)

result = client.host.conn.expect(
f"""
set pin "{pin}"
set timeout 60

spawn ipa user-add-passkey {self.name} --register
set ID_reg $spawn_id

if {{ ($pin ne "empty") }} {{
expect {{
-i $ID_reg -re "Enter PIN:*" {{}}
-i $ID_reg timeout {{puts "expect result: Unexpected output"; exit 201}}
-i $ID_reg eof {{puts "expect result: Unexpected end of file"; exit 202}}
}}

puts "Entering PIN\n"
send -i $ID_reg "{pin}\r"
}}

expect {{
-i $ID_reg -re "Please touch the device.*" {{}}
-i $ID_reg timeout {{puts "expect result: Unexpected output"; exit 203}}
-i $ID_reg eof {{puts "expect result: Unexpected end of file"; exit 204}}
}}

puts "Touching device"
spawn {test_venv_bin}/vfido_touch
set ID_touch $spawn_id

expect {{
-i $ID_reg -re "Added passkey mappings.*" {{}}
-i $ID_reg timeout {{puts "expect result: Unexpected output"; exit 205}}
-i $ID_reg eof {{puts "expect result: Unexpected end of file"; exit 206}}
}}

expect -i $ID_reg eof
expect -i $ID_touch eof
""",
raise_on_error=True,
)

return result.stdout_lines[-1].strip()

def iduseroverride(self) -> IDUserOverride:
"""
Add override to the IPA user.
Expand Down
213 changes: 209 additions & 4 deletions sssd_test_framework/utils/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from pytest_mh.utils.fs import LinuxFileSystem

from ..misc.errors import ExpectScriptError
from ..misc.globals import test_venv_bin
from .idp import IdpAuthenticationUtils

__all__ = [
Expand Down Expand Up @@ -412,8 +413,28 @@ def password_expired(self, username: str, password: str, new_password: str) -> b

def passkey_with_output(
self,
username: str,
**kwargs,
) -> tuple[int, int, str, str]:
"""wrapper for passkey_with_output methods"""
if "virt_type" in kwargs and kwargs["virt_type"] == "vfido":
return self.vfido_passkey_with_output(**kwargs)
else:
return self.umockdev_passkey_with_output(**kwargs)

def passkey(
self,
**kwargs,
) -> bool:
"""wrapper for passkey methods"""
if "virt_type" in kwargs and kwargs["virt_type"] == "vfido":
return self.vfido_passkey(**kwargs)
else:
return self.umockdev_passkey(**kwargs)

def umockdev_passkey_with_output(
self,
*,
username: str,
device: str,
ioctl: str,
script: str,
Expand Down Expand Up @@ -604,10 +625,10 @@ def passkey_with_output(

return result.rc, cmdrc, stdout, result.stderr

def passkey(
def umockdev_passkey(
self,
username: str,
*,
username: str,
device: str,
ioctl: str,
script: str,
Expand All @@ -634,11 +655,195 @@ def passkey(
:return: True if authentication was successful, False otherwise.
:rtype: bool
"""
rc, _, _, _ = self.passkey_with_output(
rc, _, _, _ = self.umockdev_passkey_with_output(
username=username, pin=pin, device=device, ioctl=ioctl, script=script, command=command
)
return rc == 0

def vfido_passkey_with_output(
self,
*,
username: str,
pin: str | int | None,
interactive_prompt: str = "Insert your passkey device, then press ENTER.",
touch_prompt: str = "Please touch the device.",
command: str = "exit 0",
auth_method: PasskeyAuthenticationUseCases = PasskeyAuthenticationUseCases.PASSKEY_PIN,
) -> tuple[int, int, str, str]:
"""
Call ``su - $username`` and authenticate the user with vfido passkey

:param username: Username
:type username: str
:param pin: Passkey PIN, defaults to None
:type pin: str | int | None
:param interactive_prompt: Interactive prompt, defaults to "Insert your passkey device, then press ENTER."
:type interactive_prompt: str
:param touch_prompt: Touch prompt, defaults to "Please touch the device."
:type touch_prompt: str
:param command: Command executed after user is authenticated, defaults to "exit 0"
:type command: str
:param auth_method: Authentication method, defaults to PasskeyAuthenticationUseCases.PASSKEY_PIN
:type auth_method: PasskeyAuthenticationUseCases
:return: Tuple containing [return code, command code, stdout, stderr].
:rtype: Tuple[int, int, str, str]
"""

match auth_method:
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we support all cases in case of GDM also?

case PasskeyAuthenticationUseCases.PASSKEY_PIN | PasskeyAuthenticationUseCases.PASSKEY_PIN_AND_PROMPTS:
if pin is None:
raise ValueError(f"PIN is required for {str(auth_method)}")
case (
PasskeyAuthenticationUseCases.PASSKEY_PROMPTS_NO_PIN
| PasskeyAuthenticationUseCases.PASSKEY_FALLBACK_TO_PASSWORD
| PasskeyAuthenticationUseCases.PASSKEY_NO_PIN_NO_PROMPTS
):
if pin is not None:
raise ValueError(f"PIN is not required for {str(auth_method)}")

run_su = self.fs.mktmp(
rf"""
#!/bin/bash
set -ex
su --shell /bin/sh nobody -c "su - '{username}' -c '{command}'"
""",
mode="a=rx",
)

result = self.host.conn.expect(
rf"""
# Disable debug output
# exp_internal 0

proc exitmsg {{ msg code }} {{
# Close spawned program, if we are in the prompt
catch close

# Wait for the exit code
lassign [wait] pid spawnid os_error_flag rc

puts ""
puts "expect result: $msg"
puts "expect exit code: $code"
puts "expect spawn exit code: $rc"
exit $code
}}

# It takes some time to get authentication failure
set timeout {DEFAULT_AUTHENTICATION_TIMEOUT}
set prompt "\n.*\[#\$>\] $"
set command "{command}"
set auth_method "{auth_method}"

spawn "{run_su}"
set ID_su $spawn_id

# If the authentication method set without entering the PIN, it will directly ask
# prompt, if we set prompting options in sssd.conf it will ask interactive and touch prompt.

if {{ ($auth_method eq "{PasskeyAuthenticationUseCases.PASSKEY_NO_PIN_NO_PROMPTS}")
|| ($auth_method eq "{PasskeyAuthenticationUseCases.PASSKEY_PROMPTS_NO_PIN}") }} {{
expect {{
-i $ID_su -re "{interactive_prompt}*" {{ send -i $ID_su "\n" }}
-i $ID_su timeout {{exitmsg "Unexpected output" 201 }}
-i $ID_su eof {{exitmsg "Unexpected end of file" 202 }}
}}
# If prompt options are set
if {{ ($auth_method eq "{PasskeyAuthenticationUseCases.PASSKEY_PROMPTS_NO_PIN}") }} {{
expect {{
-i $ID_su -re "{touch_prompt}*" {{ }}
-i $ID_su timeout {{exitmsg "Unexpected output" 203 }}
-i $ID_su eof {{exitmsg "Unexpected end of file" 204 }}
}}
}}
}}

# If authentication method set with PIN, after interactive prompt always ask to Enter the PIN.
# If PIN is correct with prompt options in sssd.conf it will ask interactive and touch prompt.
# If we press Enter key for PIN, sssd will fallback to next auth method, here it will ask
# for Password.

if {{ ($auth_method eq "{PasskeyAuthenticationUseCases.PASSKEY_PIN}")
|| ($auth_method eq "{PasskeyAuthenticationUseCases.PASSKEY_PIN_AND_PROMPTS}")
|| ($auth_method eq "{PasskeyAuthenticationUseCases.PASSKEY_FALLBACK_TO_PASSWORD}")}} {{
expect {{
-i $ID_su -re "{interactive_prompt}*" {{ send -i $ID_su "\n" }}
-i $ID_su timeout {{exitmsg "Unexpected output" 205 }}
-i $ID_su eof {{exitmsg "Unexpected end of file" 206 }}
}}
expect {{
-i $ID_su -re "Enter PIN:*" {{send -i $ID_su "{pin}\r"}}
-i $ID_su timeout {{exitmsg "Unexpected output" 207}}
-i $ID_su eof {{exitmsg "Unexpected end of file" 208}}
}}
if {{ ($auth_method eq "{PasskeyAuthenticationUseCases.PASSKEY_FALLBACK_TO_PASSWORD}") }} {{
expect {{
-i $ID_su -re "Password:*" {{send -i $ID_su "Secret123\r"}}
-i $ID_su timeout {{exitmsg "Unexpected output" 209}}
-i $ID_su eof {{exitmsg "Unexpected end of file" 210}}
}}
}}
if {{ ($auth_method eq "{PasskeyAuthenticationUseCases.PASSKEY_PIN_AND_PROMPTS}") }} {{
expect {{
-i $ID_su -re "{touch_prompt}*" {{ }}
-i $ID_su timeout {{exitmsg "Unexpected output" 211 }}
-i $ID_su eof {{exitmsg "Unexpected end of file" 212 }}
}}
}}
}}

# Now simulate touch on vfido device
spawn {test_venv_bin}/vfido_touch
set ID_touch $spawn_id

expect {{
-i $ID_su -re "Authentication failure" {{exitmsg "Authentication failure" 1}}
-i $ID_su eof {{exitmsg "Passkey authentication successful" 0}}
-i $ID_su timeout {{exitmsg "Unexpected output" 213}}
}}

expect -i $ID_touch eof

exitmsg "Unexpected code path" 220
""",
verbose=False,
)

if result.rc > 200:
raise ExpectScriptError(result.rc)

expect_data = result.stdout_lines[-3:]

# Get command exit code.
cmdrc = int(expect_data[2].split(":")[1].strip())

# Alter stdout, first line is spawned command, the last three are our expect output.
stdout = "\n".join(result.stdout_lines[1:-3])

return result.rc, cmdrc, stdout, result.stderr

def vfido_passkey(
self,
*,
username: str,
pin: str | int | None = None,
command: str = "exit 0",
) -> bool:
"""
Call ``su - $username`` and authenticate the user with passkey.

:param username: Username
:type username: str
:param pin: Passkey PIN.
:type pin: str | int | None
:param command: Command executed after user is authenticated, defaults to "exit 0"
:type command: str
:return: True if authentication was successful, False otherwise.
:rtype: bool
"""
rc, _, _, _ = self.vfido_passkey_with_output(username=username, pin=pin, command=command)
return rc == 0


class SSHAuthenticationUtils(MultihostUtility[MultihostHost]):
"""
Expand Down
1 change: 1 addition & 0 deletions sssd_test_framework/utils/gdm.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ def done(self) -> bool:

result = self.host.conn.exec(["/opt/test_venv/bin/scauto", "gui", "done"])
self.host.conn.exec(["systemctl", "stop", "gdm"], raise_on_error=False)
self.init_completed = False
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this actually needed?

return result.rc == 0

def login_idp(self, client: Client, username: str, password: str) -> bool:
Expand Down
Loading