mirror of
https://github.com/warp-tech/warpgate.git
synced 2024-09-20 06:46:17 +08:00
wip
This commit is contained in:
parent
c1faa32ab4
commit
2b92abd650
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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)?),
|
||||
};
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ impl TryFrom<Model> for User {
|
|||
username: model.username,
|
||||
roles: vec![],
|
||||
credentials,
|
||||
require: credential_policy,
|
||||
credential_policy,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
79
warpgate-web/src/admin/AuthPolicyEditor.svelte
Normal file
79
warpgate-web/src/admin/AuthPolicyEditor.svelte
Normal 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>
|
|
@ -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}
|
||||
|
|
|
@ -735,9 +735,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": ""
|
||||
},
|
||||
"404": {
|
||||
"description": ""
|
||||
}
|
||||
|
@ -1688,7 +1685,7 @@
|
|||
"$ref": "#/components/schemas/UserAuthCredential"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"credential_policy": {
|
||||
"$ref": "#/components/schemas/UserRequireCredentialsPolicy"
|
||||
},
|
||||
"roles": {
|
||||
|
|
|
@ -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()],
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in a new issue