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")]
PublicKey,
#[serde(rename = "otp")]
Otp,
Totp,
#[serde(rename = "sso")]
Sso,
#[serde(rename = "web")]
@ -38,7 +38,7 @@ impl AuthCredential {
match self {
Self::Password { .. } => CredentialKind::Password,
Self::PublicKey { .. } => CredentialKind::PublicKey,
Self::Otp { .. } => CredentialKind::Otp,
Self::Otp { .. } => CredentialKind::Totp,
Self::Sso { .. } => CredentialKind::Sso,
Self::WebUserApproval => CredentialKind::WebUserApproval,
}

View file

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

View file

@ -84,7 +84,7 @@ impl ConfigProvider for DatabaseConfigProvider {
supported_credential_types: supported_credential_types.clone(),
}) as Box<dyn CredentialPolicy + Sync + Send>;
if let Some(req) = user.require {
if let Some(req) = user.credential_policy {
let mut policy = PerProtocolCredentialPolicy {
default: default_policy,
protocols: HashMap::new(),

View file

@ -81,7 +81,7 @@ impl ConfigProvider for FileConfigProvider {
supported_credential_types: supported_credential_types.clone(),
}) as Box<dyn CredentialPolicy + Sync + Send>;
if let Some(req) = user.require {
if let Some(req) = user.credential_policy {
let mut policy = PerProtocolCredentialPolicy {
default: default_policy,
protocols: HashMap::new(),

View file

@ -244,7 +244,7 @@ async fn migrate_config_into_db(
username: Set(user_config.username.clone()),
credentials: Set(serde_json::to_value(user_config.credentials.clone())
.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)?),
};

View file

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

View file

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

View file

@ -1115,7 +1115,7 @@ impl ServerSession {
proceed_with_methods: None,
},
Ok(AuthResult::Need(kinds)) => {
if kinds.contains(&CredentialKind::Otp) {
if kinds.contains(&CredentialKind::Totp) {
self.keyboard_interactive_state = KeyboardInteractiveState::OtpRequested;
russh::server::Auth::Partial {
name: Cow::Borrowed("Two-factor authentication"),
@ -1187,7 +1187,7 @@ impl ServerSession {
for kind in kinds {
match kind {
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::PublicKey => m.insert(MethodSet::PUBLICKEY),
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">
import { faIdBadge, faKey, faKeyboard, faMobileScreen, faPhone, faPhoneAlt } from '@fortawesome/free-solid-svg-icons'
import { api, User, UserAuthCredential } from 'admin/lib/api'
import { faIdBadge, faKey, faKeyboard, faMobileScreen } from '@fortawesome/free-solid-svg-icons'
import { api, User, UserAuthCredential, UserRequireCredentialsPolicy } from 'admin/lib/api'
import AsyncButton from 'common/AsyncButton.svelte'
import DelayedSpinner from 'common/DelayedSpinner.svelte'
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 AuthPolicyEditor from './AuthPolicyEditor.svelte'
import UserCredentialModal from './UserCredentialModal.svelte'
export let params: { id: string }
@ -13,19 +14,24 @@ export let params: { id: string }
let error: Error|undefined
let user: User
let editingCredential: UserAuthCredential|undefined
let policy: UserRequireCredentialsPolicy
const policyProtocols = [
{ id: 'ssh', name: 'SSH' },
{ id: 'http', name: 'HTTP' },
{ id: 'mysql', name: 'MySQL' },
]
async function load () {
try {
user = await api.getUser({ id: params.id })
policy = user.credentialPolicy ?? {}
user.credentialPolicy = policy
} catch (err) {
error = err
}
}
function editCredential (credential: UserAuthCredential) {
}
function deleteCredential (credential) {
user.credentials = user.credentials.filter(c => c !== credential)
}
@ -131,6 +137,23 @@ async function remove () {
</div>
{/each}
</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}
{#if error}

View file

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

View file

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