mirror of
https://github.com/warp-tech/warpgate.git
synced 2025-09-07 15:14:20 +08:00
fixed #972 - SSH server not offering keyboard-interactive when only OOB or SSO auth is enabled for a user
This commit is contained in:
parent
5ee29b9ad0
commit
2381f55696
5 changed files with 52 additions and 18 deletions
|
@ -9,6 +9,11 @@ from .util import wait_port
|
||||||
|
|
||||||
|
|
||||||
class Test:
|
class Test:
|
||||||
|
# When include_pk is False, we're testing for
|
||||||
|
# https://github.com/warp-tech/warpgate/issues/972
|
||||||
|
# where the SSH server fails to offer keyboard-interactive authentication
|
||||||
|
# when no OTP credential is present.
|
||||||
|
@pytest.mark.parametrize("include_pk", [True, False])
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test(
|
async def test(
|
||||||
self,
|
self,
|
||||||
|
@ -16,6 +21,7 @@ class Test:
|
||||||
wg_c_ed25519_pubkey: Path,
|
wg_c_ed25519_pubkey: Path,
|
||||||
timeout,
|
timeout,
|
||||||
shared_wg: WarpgateProcess,
|
shared_wg: WarpgateProcess,
|
||||||
|
include_pk: bool,
|
||||||
):
|
):
|
||||||
ssh_port = processes.start_ssh_server(
|
ssh_port = processes.start_ssh_server(
|
||||||
trusted_keys=[wg_c_ed25519_pubkey.read_text()]
|
trusted_keys=[wg_c_ed25519_pubkey.read_text()]
|
||||||
|
@ -32,13 +38,24 @@ class Test:
|
||||||
api.create_password_credential(
|
api.create_password_credential(
|
||||||
user.id, sdk.NewPasswordCredential(password="123")
|
user.id, sdk.NewPasswordCredential(password="123")
|
||||||
)
|
)
|
||||||
|
if include_pk:
|
||||||
|
api.create_public_key_credential(
|
||||||
|
user.id,
|
||||||
|
sdk.NewPublicKeyCredential(
|
||||||
|
label="Public Key",
|
||||||
|
openssh_public_key=open("ssh-keys/id_ed25519.pub").read().strip()
|
||||||
|
),
|
||||||
|
)
|
||||||
api.add_user_role(user.id, role.id)
|
api.add_user_role(user.id, role.id)
|
||||||
api.update_user(
|
api.update_user(
|
||||||
user.id,
|
user.id,
|
||||||
sdk.UserDataRequest(
|
sdk.UserDataRequest(
|
||||||
username=user.username,
|
username=user.username,
|
||||||
credential_policy=sdk.UserRequireCredentialsPolicy(
|
credential_policy=sdk.UserRequireCredentialsPolicy(
|
||||||
ssh=[sdk.CredentialKind.WEBUSERAPPROVAL],
|
ssh=[sdk.CredentialKind.WEBUSERAPPROVAL] if not include_pk else [
|
||||||
|
sdk.CredentialKind.PUBLICKEY,
|
||||||
|
sdk.CredentialKind.WEBUSERAPPROVAL,
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -80,11 +97,10 @@ class Test:
|
||||||
f"{user.username}:{ssh_target.name}@localhost",
|
f"{user.username}:{ssh_target.name}@localhost",
|
||||||
"-p",
|
"-p",
|
||||||
str(shared_wg.ssh_port),
|
str(shared_wg.ssh_port),
|
||||||
"-i",
|
"-o",
|
||||||
"/dev/null",
|
"IdentityFile=ssh-keys/id_ed25519",
|
||||||
"ls",
|
"ls",
|
||||||
"/bin/sh",
|
"/bin/sh",
|
||||||
password="123",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
msg = await ws.receive(5)
|
msg = await ws.receive(5)
|
||||||
|
|
|
@ -2,6 +2,7 @@ from asyncio import subprocess
|
||||||
from base64 import b64decode
|
from base64 import b64decode
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
import pyotp
|
import pyotp
|
||||||
|
import pytest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
|
||||||
|
@ -32,13 +33,14 @@ class Test:
|
||||||
sdk.RoleDataRequest(name=f"role-{uuid4()}"),
|
sdk.RoleDataRequest(name=f"role-{uuid4()}"),
|
||||||
)
|
)
|
||||||
user = api.create_user(sdk.CreateUserRequest(username=f"user-{uuid4()}"))
|
user = api.create_user(sdk.CreateUserRequest(username=f"user-{uuid4()}"))
|
||||||
api.create_public_key_credential(
|
if include_public_key:
|
||||||
user.id,
|
api.create_public_key_credential(
|
||||||
sdk.NewPublicKeyCredential(
|
user.id,
|
||||||
label="Public Key",
|
sdk.NewPublicKeyCredential(
|
||||||
openssh_public_key=open("ssh-keys/id_ed25519.pub").read().strip()
|
label="Public Key",
|
||||||
),
|
openssh_public_key=open("ssh-keys/id_ed25519.pub").read().strip(),
|
||||||
)
|
),
|
||||||
|
)
|
||||||
api.create_otp_credential(
|
api.create_otp_credential(
|
||||||
user.id,
|
user.id,
|
||||||
sdk.NewOtpCredential(
|
sdk.NewOtpCredential(
|
||||||
|
|
|
@ -109,12 +109,21 @@ impl ConfigProvider for DatabaseConfigProvider {
|
||||||
|
|
||||||
let user = user_model.load_details(&db).await?;
|
let user = user_model.load_details(&db).await?;
|
||||||
|
|
||||||
let supported_credential_types: HashSet<CredentialKind> = user
|
let mut available_credential_types = user
|
||||||
.credentials
|
.credentials
|
||||||
.iter()
|
.iter()
|
||||||
.map(|x| x.kind())
|
.map(|x| x.kind())
|
||||||
.filter(|x| supported_credential_types.contains(x))
|
.collect::<HashSet<_>>();
|
||||||
.collect();
|
available_credential_types.insert(CredentialKind::WebUserApproval);
|
||||||
|
|
||||||
|
let supported_credential_types = supported_credential_types
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.collect::<HashSet<_>>()
|
||||||
|
.intersection(&available_credential_types)
|
||||||
|
.copied()
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
let default_policy = Box::new(AnySingleCredentialPolicy {
|
let default_policy = Box::new(AnySingleCredentialPolicy {
|
||||||
supported_credential_types: supported_credential_types.clone(),
|
supported_credential_types: supported_credential_types.clone(),
|
||||||
}) as Box<dyn CredentialPolicy + Sync + Send>;
|
}) as Box<dyn CredentialPolicy + Sync + Send>;
|
||||||
|
|
|
@ -1497,6 +1497,10 @@ impl ServerSession {
|
||||||
CredentialKind::Sso => m.push(MethodKind::KeyboardInteractive),
|
CredentialKind::Sso => m.push(MethodKind::KeyboardInteractive),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if m.contains(&MethodKind::KeyboardInteractive) {
|
||||||
|
// Ensure keyboard-interactive is always the last method
|
||||||
|
m.push(MethodKind::KeyboardInteractive);
|
||||||
|
}
|
||||||
m
|
m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Input } from '@sveltestrap/sveltestrap'
|
import { Input } from '@sveltestrap/sveltestrap'
|
||||||
import { CredentialKind, type UserRequireCredentialsPolicy } from './lib/api'
|
import { CredentialKind, type UserRequireCredentialsPolicy } from './lib/api'
|
||||||
import type { ExistingCredential } from './CredentialEditor.svelte'
|
import type { ExistingCredential } from './CredentialEditor.svelte'
|
||||||
import Fa from 'svelte-fa';
|
import Fa from 'svelte-fa'
|
||||||
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
|
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
type ProtocolID = 'http' | 'ssh' | 'mysql' | 'postgres'
|
type ProtocolID = 'http' | 'ssh' | 'mysql' | 'postgres'
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ const labels = {
|
||||||
WebUserApproval: 'In-browser auth',
|
WebUserApproval: 'In-browser auth',
|
||||||
}
|
}
|
||||||
|
|
||||||
const tips: Record<ProtocolID, Map<[CredentialKind, boolean], string> | undefined> = {
|
const tips: Record<ProtocolID, Map<[CredentialKind, boolean], string>> = {
|
||||||
postgres: new Map([
|
postgres: new Map([
|
||||||
[
|
[
|
||||||
[CredentialKind.Password, false],
|
[CredentialKind.Password, false],
|
||||||
|
@ -40,6 +40,9 @@ const tips: Record<ProtocolID, Map<[CredentialKind, boolean], string> | undefine
|
||||||
'Not all clients will show the 2FA auth prompt. The user might need to log in to the Warpgate UI to see the prompt.',
|
'Not all clients will show the 2FA auth prompt. The user might need to log in to the Warpgate UI to see the prompt.',
|
||||||
],
|
],
|
||||||
]),
|
]),
|
||||||
|
http: new Map(),
|
||||||
|
mysql: new Map(),
|
||||||
|
ssh: new Map(),
|
||||||
}
|
}
|
||||||
|
|
||||||
let activeTips: string[] = $derived.by(() => {
|
let activeTips: string[] = $derived.by(() => {
|
||||||
|
|
Loading…
Add table
Reference in a new issue