Skip to content

Commit

Permalink
beiboot: Send initial authorize request
Browse files Browse the repository at this point in the history
In order to use cockpit.beiboot as cockpit-ssh replacement from the
bastion host (not Client mode) login page, it needs to consider the given
username and password. cockpit-ssh sends an initial `authorize` message
for that and checks for "Basic" auth. If that fails, it aborts
immediately with `authentication-failed`. Implement the same in
cockpit.beiboot.

Note: The UI does not currently get along with multiple password
attempts. Once we drop cockpit-ssh, we should fix the UI and
cockpit.beiboot to behave like the flatpak, keep the initial SSH
running, and just answer the "try again" prompts.

Cover this in a new `TestLogin.testLoginSshBeiboot`. Once we generally
replace cockpit-ssh with cockpit.beiboot, this will get absorbed by
TestLogin and TestMultiMachine* and can be dropped again.
  • Loading branch information
martinpitt committed Sep 23, 2024
1 parent 59bc6d8 commit c4b1956
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 2 deletions.
19 changes: 17 additions & 2 deletions src/cockpit/beiboot.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,25 @@ class AuthorizeResponder(ferny.AskpassHandler):
commands = ('ferny.askpass', 'cockpit.report-exists')
router: Router

def __init__(self, router: Router):
def __init__(self, router: Router, basic_password: Optional[str]):
self.router = router
self.basic_password = basic_password
self.have_basic_password = basic_password is not None

async def do_askpass(self, messages: str, prompt: str, hint: str) -> Optional[str]:
logger.debug("AuthorizeResponder: prompt %r, messages %r, hint %r", prompt, messages, hint)

if self.have_basic_password and 'password:' in prompt.lower():
# with our NumberOfPasswordPrompts=1 ssh should never actually ask us more than once; assert that
if self.basic_password is None:
raise CockpitProtocolError(
f"ssh asked for password a second time, but we already sent it; prompt: {messages}")

logger.debug("AuthorizeResponder: sending Basic auth password for prompt %r", prompt)
reply = self.basic_password
self.basic_password = None
return reply

if hint == 'none':
# We have three problems here:
#
Expand Down Expand Up @@ -281,7 +296,7 @@ async def connect_from_bastion_host(self) -> None:

async def boot(self, cmd: Sequence[str], env: Sequence[str], basic_password: 'str | None' = None) -> None:
beiboot_helper = BridgeBeibootHelper(self)
agent = ferny.InteractionAgent([AuthorizeResponder(self.router), beiboot_helper])
agent = ferny.InteractionAgent([AuthorizeResponder(self.router, basic_password), beiboot_helper])

logger.debug("Launching command: cmd=%s env=%s", cmd, env)
transport = await self.spawn(cmd, env, stderr=agent, start_new_session=True)
Expand Down
1 change: 1 addition & 0 deletions test/browser/run-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ if [ "$PLAN" = "main" ]; then
TestLogin.testFailingWebsocketSafari
TestLogin.testFailingWebsocketSafariNoCA
TestLogin.testLogging
TestLogin.testLoginSshBeiboot
TestLogin.testRaw
TestLogin.testServer
TestLogin.testUnsupportedBrowser
Expand Down
77 changes: 77 additions & 0 deletions test/verify/check-static-login
Original file line number Diff line number Diff line change
Expand Up @@ -954,6 +954,83 @@ matchrule = <SUBJECT>^DC=LAN,DC=COCKPIT,CN=alice$
# by IPv6 address and port
check_server("[::1]:22", expect_fp_ack=True)

# check cockpit-beiboot as replacement of cockpit-ssh on the login page
# once that becomes the default, TestMultiMachine* and the other TestLogin* cover this
@testlib.skipImage("needs pybridge", "rhel-8*", "centos-8*")
# enable this once our cockpit/ws container can beiboot
@testlib.skipOstree("client setup does not work with ws container")
def testLoginSshBeiboot(self):
m = self.machine
b = self.browser

# this matches our bots test VMs
my_ip = "172.27.0.15"
m.write("/etc/cockpit/cockpit.conf", """
[Ssh-Login]
Command = /usr/bin/env python3 -m cockpit.beiboot
""", append=True)
m.start_cockpit()

def try_login(user, password, server=None):
b.open("/")
b.set_val('#login-user-input', user)
b.set_val('#login-password-input', password)
b.click("#show-other-login-options")
b.set_val("#server-field", server or my_ip)
b.click("#login-button")
# ack unknown host key; FIXME: this should be a proper authorize message, not a prompt
b.wait_in_text("#conversation-prompt", "authenticity of host")
b.set_val("#conversation-input", "yes")
b.click("#login-button")

def check_no_processes():
m.execute(f"while pgrep -af '[s]sh .* {my_ip}' >&2; do sleep 1; done")
m.execute("while pgrep -af '[c]ockpit.beiboot' >&2; do sleep 1; done")

def check_session(server=None):
b.wait_visible('#content')
b.enter_page('/system')
b.wait_visible('.system-information')
m.execute(f"pgrep -af '[s]sh .* -l admin .*{server or my_ip}'")
m.execute(f"pgrep -af '[c]ockpit.beiboot.*{server or my_ip}'")
b.logout()
check_no_processes()

# successful login through SSH
try_login("admin", "foobar")
check_session()

# wrong password
try_login("admin", "wrong")
b.wait_in_text("#login-error-message", "Authentication failed")
check_no_processes()
# goes back to normal login form
b.wait_visible('#login-user-input')

# colliding usernames; user names in "Connect to:" are *not* supported,
# but pin down the behaviour
try_login("admin", "foobar", server=f"other@{my_ip}")
check_session()

# IPv6
try_login("admin", "foobar", server="::1")
check_session(server="::1")
try_login("admin", "foobar", server="[::1]:22")
check_session(server="::1")

# empty password
self.write_file("/etc/ssh/sshd_config.d/01-empty-password.conf", "PermitEmptyPasswords yes",
post_restore_action=self.restart_sshd)
m.execute(self.restart_sshd)
m.execute("passwd -d admin")
try_login("admin", "")
check_session()
m.execute("echo 'admin:foobar' | chpasswd")
if m.image == 'arch':
# HACK: PermitEmptyPasswords somehow breaks regular password auth
m.execute("rm /etc/ssh/sshd_config.d/01-empty-password.conf")
m.execute(self.restart_sshd)


if __name__ == '__main__':
testlib.test_main()

0 comments on commit c4b1956

Please sign in to comment.