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")]
|
#[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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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)?),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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),
|
||||||
|
|
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">
|
<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}
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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()],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue