This commit is contained in:
Eugene Pankov 2022-08-30 22:21:04 +02:00
parent c1faa32ab4
commit 2b92abd650
No known key found for this signature in database
GPG key ID: 5896FCBBDD1CF4F4
12 changed files with 123 additions and 24 deletions

View file

@ -11,7 +11,7 @@ pub enum CredentialKind {
#[serde(rename = "publickey")] #[serde(rename = "publickey")]
PublicKey, PublicKey,
#[serde(rename = "otp")] #[serde(rename = "otp")]
Otp, Totp,
#[serde(rename = "sso")] #[serde(rename = "sso")]
Sso, Sso,
#[serde(rename = "web")] #[serde(rename = "web")]
@ -38,7 +38,7 @@ impl AuthCredential {
match self { match self {
Self::Password { .. } => CredentialKind::Password, Self::Password { .. } => CredentialKind::Password,
Self::PublicKey { .. } => CredentialKind::PublicKey, Self::PublicKey { .. } => CredentialKind::PublicKey,
Self::Otp { .. } => CredentialKind::Otp, Self::Otp { .. } => CredentialKind::Totp,
Self::Sso { .. } => CredentialKind::Sso, Self::Sso { .. } => CredentialKind::Sso,
Self::WebUserApproval => CredentialKind::WebUserApproval, Self::WebUserApproval => CredentialKind::WebUserApproval,
} }

View file

@ -54,7 +54,7 @@ impl UserAuthCredential {
match self { match self {
Self::Password(_) => CredentialKind::Password, Self::Password(_) => CredentialKind::Password,
Self::PublicKey(_) => CredentialKind::PublicKey, Self::PublicKey(_) => CredentialKind::PublicKey,
Self::Totp(_) => CredentialKind::Otp, Self::Totp(_) => CredentialKind::Totp,
Self::Sso(_) => CredentialKind::Sso, Self::Sso(_) => CredentialKind::Sso,
} }
} }
@ -76,8 +76,8 @@ pub struct User {
pub id: Uuid, pub id: Uuid,
pub username: String, pub username: String,
pub credentials: Vec<UserAuthCredential>, pub credentials: Vec<UserAuthCredential>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none", rename="require")]
pub require: Option<UserRequireCredentialsPolicy>, pub credential_policy: Option<UserRequireCredentialsPolicy>,
pub roles: Vec<String>, pub roles: Vec<String>,
} }

View file

@ -84,7 +84,7 @@ impl ConfigProvider for DatabaseConfigProvider {
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>;
if let Some(req) = user.require { if let Some(req) = user.credential_policy {
let mut policy = PerProtocolCredentialPolicy { let mut policy = PerProtocolCredentialPolicy {
default: default_policy, default: default_policy,
protocols: HashMap::new(), protocols: HashMap::new(),

View file

@ -81,7 +81,7 @@ impl ConfigProvider for FileConfigProvider {
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>;
if let Some(req) = user.require { if let Some(req) = user.credential_policy {
let mut policy = PerProtocolCredentialPolicy { let mut policy = PerProtocolCredentialPolicy {
default: default_policy, default: default_policy,
protocols: HashMap::new(), protocols: HashMap::new(),

View file

@ -244,7 +244,7 @@ async fn migrate_config_into_db(
username: Set(user_config.username.clone()), username: Set(user_config.username.clone()),
credentials: Set(serde_json::to_value(user_config.credentials.clone()) credentials: Set(serde_json::to_value(user_config.credentials.clone())
.map_err(WarpgateError::from)?), .map_err(WarpgateError::from)?),
credential_policy: Set(serde_json::to_value(user_config.require.clone()) credential_policy: Set(serde_json::to_value(user_config.credential_policy.clone())
.map_err(WarpgateError::from)?), .map_err(WarpgateError::from)?),
}; };

View file

@ -42,7 +42,7 @@ impl TryFrom<Model> for User {
username: model.username, username: model.username,
roles: vec![], roles: vec![],
credentials, credentials,
require: credential_policy, credential_policy,
}) })
} }
} }

View file

@ -83,7 +83,7 @@ impl From<AuthResult> for ApiAuthState {
AuthResult::Rejected => ApiAuthState::Failed, AuthResult::Rejected => ApiAuthState::Failed,
AuthResult::Need(kinds) => match kinds.iter().next() { AuthResult::Need(kinds) => match kinds.iter().next() {
Some(CredentialKind::Password) => ApiAuthState::PasswordNeeded, Some(CredentialKind::Password) => ApiAuthState::PasswordNeeded,
Some(CredentialKind::Otp) => ApiAuthState::OtpNeeded, Some(CredentialKind::Totp) => ApiAuthState::OtpNeeded,
Some(CredentialKind::Sso) => ApiAuthState::SsoNeeded, Some(CredentialKind::Sso) => ApiAuthState::SsoNeeded,
Some(CredentialKind::WebUserApproval) => ApiAuthState::WebUserApprovalNeeded, Some(CredentialKind::WebUserApproval) => ApiAuthState::WebUserApprovalNeeded,
Some(CredentialKind::PublicKey) => ApiAuthState::PublicKeyNeeded, Some(CredentialKind::PublicKey) => ApiAuthState::PublicKeyNeeded,

View file

@ -1115,7 +1115,7 @@ impl ServerSession {
proceed_with_methods: None, proceed_with_methods: None,
}, },
Ok(AuthResult::Need(kinds)) => { Ok(AuthResult::Need(kinds)) => {
if kinds.contains(&CredentialKind::Otp) { if kinds.contains(&CredentialKind::Totp) {
self.keyboard_interactive_state = KeyboardInteractiveState::OtpRequested; self.keyboard_interactive_state = KeyboardInteractiveState::OtpRequested;
russh::server::Auth::Partial { russh::server::Auth::Partial {
name: Cow::Borrowed("Two-factor authentication"), name: Cow::Borrowed("Two-factor authentication"),
@ -1187,7 +1187,7 @@ impl ServerSession {
for kind in kinds { for kind in kinds {
match kind { match kind {
CredentialKind::Password => m.insert(MethodSet::PASSWORD), CredentialKind::Password => m.insert(MethodSet::PASSWORD),
CredentialKind::Otp => m.insert(MethodSet::KEYBOARD_INTERACTIVE), CredentialKind::Totp => m.insert(MethodSet::KEYBOARD_INTERACTIVE),
CredentialKind::WebUserApproval => m.insert(MethodSet::KEYBOARD_INTERACTIVE), CredentialKind::WebUserApproval => m.insert(MethodSet::KEYBOARD_INTERACTIVE),
CredentialKind::PublicKey => m.insert(MethodSet::PUBLICKEY), CredentialKind::PublicKey => m.insert(MethodSet::PUBLICKEY),
CredentialKind::Sso => m.insert(MethodSet::KEYBOARD_INTERACTIVE), CredentialKind::Sso => m.insert(MethodSet::KEYBOARD_INTERACTIVE),

View file

@ -0,0 +1,79 @@
<script lang="ts">
import { Input } from 'sveltestrap'
import type { User, UserRequireCredentialsPolicy } from './lib/api'
export let user: User
export let value: UserRequireCredentialsPolicy
export let protocolId: string
const labels = {
Password: 'Password',
PublicKey: 'Key',
Totp: 'OTP',
Sso: 'SSO',
WebUserApproval: 'In-browser auth',
}
let isAny = !value[protocolId]
let validCredentials = new Set<string>()
const possibleCredentials = {
ssh: new Set(['Password', 'PublicKey', 'Totp', 'WebUserApproval']),
http: new Set(['Password', 'Totp', 'Sso', 'WebUserApproval']),
mysql: new Set(['Password']),
}[protocolId]!
$: {
validCredentials = new Set(user.credentials.map(x => x.kind))
validCredentials.add('WebUserApproval')
}
function updateAny () {
setTimeout(() => {
if (isAny) {
value[protocolId] = undefined
} else {
value[protocolId] = [Array.from(validCredentials)[0]]
}
})
}
function toggle (type: string) {
if (value[protocolId].includes(type)) {
value[protocolId] = value[protocolId].filter(x => x !== type)
} else {
value[protocolId].push(type)
}
}
</script>
<div class="d-flex wrapper">
<Input
id={'policy-editor-' + user.username + protocolId}
type="switch"
bind:checked={isAny}
label="Any credential"
on:change={updateAny}
/>
{#if !isAny}
{#each [...validCredentials] as type}
{#if possibleCredentials.has(type)}
<Input
id={'policy-editor-' + user.username + protocolId + type}
type="switch"
checked={value[protocolId]?.includes(type)}
label={labels[type]}
on:change={() => toggle(type)}
/>
{/if}
{/each}
{/if}
</div>
<style lang="scss">
.wrapper {
flex-wrap: wrap;
:global(.form-switch) {
margin-right: 1rem;
}
}
</style>

View file

@ -1,11 +1,12 @@
<script lang="ts"> <script lang="ts">
import { faIdBadge, faKey, faKeyboard, faMobileScreen, faPhone, faPhoneAlt } from '@fortawesome/free-solid-svg-icons' import { faIdBadge, faKey, faKeyboard, faMobileScreen } from '@fortawesome/free-solid-svg-icons'
import { api, User, UserAuthCredential } from 'admin/lib/api' import { api, User, UserAuthCredential, UserRequireCredentialsPolicy } from 'admin/lib/api'
import AsyncButton from 'common/AsyncButton.svelte' import AsyncButton from 'common/AsyncButton.svelte'
import DelayedSpinner from 'common/DelayedSpinner.svelte' import DelayedSpinner from 'common/DelayedSpinner.svelte'
import Fa from 'svelte-fa' import Fa from 'svelte-fa'
import { push, replace } from 'svelte-spa-router' import { replace } from 'svelte-spa-router'
import { Alert, Button, FormGroup, Input } from 'sveltestrap' import { Alert, Button, FormGroup, Input } from 'sveltestrap'
import AuthPolicyEditor from './AuthPolicyEditor.svelte'
import UserCredentialModal from './UserCredentialModal.svelte' import UserCredentialModal from './UserCredentialModal.svelte'
export let params: { id: string } export let params: { id: string }
@ -13,19 +14,24 @@ export let params: { id: string }
let error: Error|undefined let error: Error|undefined
let user: User let user: User
let editingCredential: UserAuthCredential|undefined let editingCredential: UserAuthCredential|undefined
let policy: UserRequireCredentialsPolicy
const policyProtocols = [
{ id: 'ssh', name: 'SSH' },
{ id: 'http', name: 'HTTP' },
{ id: 'mysql', name: 'MySQL' },
]
async function load () { async function load () {
try { try {
user = await api.getUser({ id: params.id }) user = await api.getUser({ id: params.id })
policy = user.credentialPolicy ?? {}
user.credentialPolicy = policy
} catch (err) { } catch (err) {
error = err error = err
} }
} }
function editCredential (credential: UserAuthCredential) {
}
function deleteCredential (credential) { function deleteCredential (credential) {
user.credentials = user.credentials.filter(c => c !== credential) user.credentials = user.credentials.filter(c => c !== credential)
} }
@ -131,6 +137,23 @@ async function remove () {
</div> </div>
{/each} {/each}
</div> </div>
<h4>Auth policy</h4>
<div class="list-group list-group-flush mb-3">
{#each policyProtocols as protocol}
<div class="list-group-item">
<div>
<strong>{protocol.name}</strong>
</div>
<AuthPolicyEditor
user={user}
bind:value={policy}
protocolId={protocol.id}
/>
</div>
{/each}
</div>
{/await} {/await}
{#if error} {#if error}

View file

@ -735,9 +735,6 @@
} }
} }
}, },
"400": {
"description": ""
},
"404": { "404": {
"description": "" "description": ""
} }
@ -1688,7 +1685,7 @@
"$ref": "#/components/schemas/UserAuthCredential" "$ref": "#/components/schemas/UserAuthCredential"
} }
}, },
"require": { "credential_policy": {
"$ref": "#/components/schemas/UserRequireCredentialsPolicy" "$ref": "#/components/schemas/UserRequireCredentialsPolicy"
}, },
"roles": { "roles": {

View file

@ -197,7 +197,7 @@ pub(crate) async fn command(cli: &crate::Cli) -> Result<()> {
credentials: vec![UserAuthCredential::Password(UserPasswordCredential { credentials: vec![UserAuthCredential::Password(UserPasswordCredential {
hash: Secret::new(hash_password(&password)), hash: Secret::new(hash_password(&password)),
})], })],
require: None, credential_policy: None,
roles: vec![BUILTIN_ADMIN_ROLE_NAME.into()], roles: vec![BUILTIN_ADMIN_ROLE_NAME.into()],
}); });