mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
383 170 164 authentication updates - credential update webui! (#809)
This commit is contained in:
parent
0d510baebb
commit
b97d13d284
38
Cargo.lock
generated
38
Cargo.lock
generated
|
@ -394,6 +394,16 @@ version = "0.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
|
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64urlsafedata"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b080c44c552d34075dba9dfd9df2b150459fdbc25622180bcb7e541d2bf01283"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.13.0",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bincode"
|
name = "bincode"
|
||||||
version = "1.3.3"
|
version = "1.3.3"
|
||||||
|
@ -618,11 +628,12 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "compact_jwt"
|
name = "compact_jwt"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c808f92e2decd79e113f0a6214a495e3ec830c77b78e7d5f7a41a28299165cc0"
|
checksum = "469e39800a5e0d942409092349cfeb6189bdf2393d13fad535cb681424464623"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.13.0",
|
"base64 0.13.0",
|
||||||
|
"base64urlsafedata",
|
||||||
"openssl",
|
"openssl",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
@ -2047,6 +2058,7 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"url",
|
||||||
"webauthn-authenticator-rs",
|
"webauthn-authenticator-rs",
|
||||||
"zxcvbn",
|
"zxcvbn",
|
||||||
]
|
]
|
||||||
|
@ -2085,9 +2097,11 @@ dependencies = [
|
||||||
name = "kanidmd_web_ui"
|
name = "kanidmd_web_ui"
|
||||||
version = "1.1.0-alpha.8"
|
version = "1.1.0-alpha.8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"compact_jwt",
|
||||||
"gloo 0.7.0",
|
"gloo 0.7.0",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"kanidm_proto",
|
"kanidm_proto",
|
||||||
|
"qrcode",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
|
@ -2095,6 +2109,7 @@ dependencies = [
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"webauthn-rs",
|
"webauthn-rs",
|
||||||
"yew",
|
"yew",
|
||||||
|
"yew-agent",
|
||||||
"yew-router",
|
"yew-router",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -4464,6 +4479,25 @@ dependencies = [
|
||||||
"yew-macro",
|
"yew-macro",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yew-agent"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "616700dc3851945658c44ba4477ede6b77c795462fbbb9b0ad9a8b6273a3ca77"
|
||||||
|
dependencies = [
|
||||||
|
"anymap2",
|
||||||
|
"bincode",
|
||||||
|
"gloo-console",
|
||||||
|
"gloo-utils",
|
||||||
|
"js-sys",
|
||||||
|
"serde",
|
||||||
|
"slab",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
"yew",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yew-macro"
|
name = "yew-macro"
|
||||||
version = "0.19.3"
|
version = "0.19.3"
|
||||||
|
|
|
@ -1627,7 +1627,7 @@ impl KanidmClient {
|
||||||
pub async fn idm_account_credential_update_begin(
|
pub async fn idm_account_credential_update_begin(
|
||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: &str,
|
||||||
) -> Result<CUSessionToken, ClientError> {
|
) -> Result<(CUSessionToken, CUStatus), ClientError> {
|
||||||
self.perform_get_request(format!("/v1/account/{}/_credential/_update", id).as_str())
|
self.perform_get_request(format!("/v1/account/{}/_credential/_update", id).as_str())
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
@ -1635,7 +1635,7 @@ impl KanidmClient {
|
||||||
pub async fn idm_account_credential_update_exchange(
|
pub async fn idm_account_credential_update_exchange(
|
||||||
&self,
|
&self,
|
||||||
intent_token: CUIntentToken,
|
intent_token: CUIntentToken,
|
||||||
) -> Result<CUSessionToken, ClientError> {
|
) -> Result<(CUSessionToken, CUStatus), ClientError> {
|
||||||
// We don't need to send the UAT with these, which is why we use the different path.
|
// We don't need to send the UAT with these, which is why we use the different path.
|
||||||
self.perform_simple_post_request("/v1/credential/_exchange_intent", &intent_token)
|
self.perform_simple_post_request("/v1/credential/_exchange_intent", &intent_token)
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -825,12 +825,12 @@ pub enum SetCredentialResponse {
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct CUIntentToken {
|
pub struct CUIntentToken {
|
||||||
pub intent_token: String,
|
pub token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct CUSessionToken {
|
pub struct CUSessionToken {
|
||||||
pub session_token: String,
|
pub token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
@ -864,7 +864,7 @@ impl fmt::Debug for CURequest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum CURegState {
|
pub enum CURegState {
|
||||||
// Nothing in progress.
|
// Nothing in progress.
|
||||||
None,
|
None,
|
||||||
|
@ -874,8 +874,10 @@ pub enum CURegState {
|
||||||
BackupCodes(Vec<String>),
|
BackupCodes(Vec<String>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct CUStatus {
|
pub struct CUStatus {
|
||||||
|
pub spn: String,
|
||||||
|
pub displayname: String,
|
||||||
pub can_commit: bool,
|
pub can_commit: bool,
|
||||||
pub primary: Option<CredentialDetail>,
|
pub primary: Option<CredentialDetail>,
|
||||||
pub mfaregstate: CURegState,
|
pub mfaregstate: CURegState,
|
||||||
|
|
|
@ -42,7 +42,7 @@ shellexpand = "^2.1.0"
|
||||||
rayon = "^1.5.3"
|
rayon = "^1.5.3"
|
||||||
time = { version = "=0.2.27", features = ["serde", "std"] }
|
time = { version = "=0.2.27", features = ["serde", "std"] }
|
||||||
qrcode = { version = "^0.12.0", default-features = false }
|
qrcode = { version = "^0.12.0", default-features = false }
|
||||||
compact_jwt = "^0.2.1"
|
compact_jwt = "^0.2.2"
|
||||||
|
|
||||||
zxcvbn = "^2.2.1"
|
zxcvbn = "^2.2.1"
|
||||||
|
|
||||||
|
@ -50,6 +50,7 @@ dialoguer = "^0.10.1"
|
||||||
|
|
||||||
webauthn-authenticator-rs = "^0.3.0-alpha.12"
|
webauthn-authenticator-rs = "^0.3.0-alpha.12"
|
||||||
tokio = { version = "^1.18.0", features = ["rt", "macros"] }
|
tokio = { version = "^1.18.0", features = ["rt", "macros"] }
|
||||||
|
url = { version = "^2.2.2", features = ["serde"] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
structopt = { version = "^0.3.26", default-features = false }
|
structopt = { version = "^0.3.26", default-features = false }
|
||||||
|
|
|
@ -19,6 +19,7 @@ use kanidm_proto::v1::OperationError::PasswordQuality;
|
||||||
use kanidm_proto::v1::{CUIntentToken, CURegState, CUSessionToken, CUStatus};
|
use kanidm_proto::v1::{CUIntentToken, CURegState, CUSessionToken, CUStatus};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
impl AccountOpt {
|
impl AccountOpt {
|
||||||
pub fn debug(&self) -> bool {
|
pub fn debug(&self) -> bool {
|
||||||
|
@ -393,7 +394,9 @@ impl AccountCredential {
|
||||||
.idm_account_credential_update_begin(aopt.aopts.account_id.as_str())
|
.idm_account_credential_update_begin(aopt.aopts.account_id.as_str())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(cusession_token) => credential_update_exec(cusession_token, client).await,
|
Ok((cusession_token, custatus)) => {
|
||||||
|
credential_update_exec(cusession_token, custatus, client).await
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error starting credential update -> {:?}", e);
|
error!("Error starting credential update -> {:?}", e);
|
||||||
}
|
}
|
||||||
|
@ -402,14 +405,16 @@ impl AccountCredential {
|
||||||
AccountCredential::Reset(aopt) => {
|
AccountCredential::Reset(aopt) => {
|
||||||
let client = aopt.copt.to_unauth_client();
|
let client = aopt.copt.to_unauth_client();
|
||||||
let cuintent_token = CUIntentToken {
|
let cuintent_token = CUIntentToken {
|
||||||
intent_token: aopt.token.clone(),
|
token: aopt.token.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
match client
|
match client
|
||||||
.idm_account_credential_update_exchange(cuintent_token)
|
.idm_account_credential_update_exchange(cuintent_token)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(cusession_token) => credential_update_exec(cusession_token, client).await,
|
Ok((cusession_token, custatus)) => {
|
||||||
|
credential_update_exec(cusession_token, custatus, client).await
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error starting credential reset -> {:?}", e);
|
error!("Error starting credential reset -> {:?}", e);
|
||||||
}
|
}
|
||||||
|
@ -417,18 +422,44 @@ impl AccountCredential {
|
||||||
}
|
}
|
||||||
AccountCredential::CreateResetLink(aopt) => {
|
AccountCredential::CreateResetLink(aopt) => {
|
||||||
let client = aopt.copt.to_client().await;
|
let client = aopt.copt.to_client().await;
|
||||||
|
|
||||||
|
// What's the client url?
|
||||||
match client
|
match client
|
||||||
.idm_account_credential_update_intent(aopt.aopts.account_id.as_str())
|
.idm_account_credential_update_intent(aopt.aopts.account_id.as_str())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(cuintent_token) => {
|
Ok(cuintent_token) => {
|
||||||
|
let mut url = Url::parse(client.get_url()).expect("Invalid server url.");
|
||||||
|
url.set_path("/ui/reset");
|
||||||
|
url.query_pairs_mut()
|
||||||
|
.append_pair("token", cuintent_token.token.as_str());
|
||||||
|
|
||||||
println!("success!");
|
println!("success!");
|
||||||
println!("Send the person the following command");
|
|
||||||
println!("");
|
|
||||||
println!(
|
println!(
|
||||||
"kanidm account credential reset link {}",
|
"Send the person one of the following to allow the credential reset"
|
||||||
cuintent_token.intent_token
|
|
||||||
);
|
);
|
||||||
|
println!("scan:");
|
||||||
|
let code = match QrCode::new(url.as_str()) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to generate QR code -> {:?}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let image = code
|
||||||
|
.render::<unicode::Dense1x2>()
|
||||||
|
.dark_color(unicode::Dense1x2::Light)
|
||||||
|
.light_color(unicode::Dense1x2::Dark)
|
||||||
|
.build();
|
||||||
|
println!("{}", image);
|
||||||
|
|
||||||
|
println!("");
|
||||||
|
println!("link: {}", url.as_str());
|
||||||
|
println!(
|
||||||
|
"command: kanidm account credential reset {}",
|
||||||
|
cuintent_token.token
|
||||||
|
);
|
||||||
|
println!("");
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error starting credential reset -> {:?}", e);
|
error!("Error starting credential reset -> {:?}", e);
|
||||||
|
@ -702,8 +733,41 @@ async fn totp_enroll_prompt(session_token: &CUSessionToken, client: &KanidmClien
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async fn credential_update_exec(session_token: CUSessionToken, client: KanidmClient) {
|
fn display_status(status: CUStatus) {
|
||||||
|
let CUStatus {
|
||||||
|
spn,
|
||||||
|
displayname,
|
||||||
|
can_commit,
|
||||||
|
primary,
|
||||||
|
mfaregstate: _,
|
||||||
|
} = status;
|
||||||
|
|
||||||
|
println!("spn: {}", spn);
|
||||||
|
println!("Name: {}", displayname);
|
||||||
|
if let Some(cred_detail) = &primary {
|
||||||
|
println!("Primary Credential:");
|
||||||
|
print!("{}", cred_detail);
|
||||||
|
} else {
|
||||||
|
println!("Primary Credential:");
|
||||||
|
println!(" not set");
|
||||||
|
}
|
||||||
|
|
||||||
|
// We may need to be able to display if there are dangling
|
||||||
|
// curegstates, but the cli ui statemachine can match the
|
||||||
|
// server so it may not be needed?
|
||||||
|
|
||||||
|
println!("Can Commit: {}", can_commit);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn credential_update_exec(
|
||||||
|
session_token: CUSessionToken,
|
||||||
|
status: CUStatus,
|
||||||
|
client: KanidmClient,
|
||||||
|
) {
|
||||||
trace!("started credential update exec");
|
trace!("started credential update exec");
|
||||||
|
// Show the initial status,
|
||||||
|
display_status(status);
|
||||||
|
// Setup to work
|
||||||
loop {
|
loop {
|
||||||
// Display Prompt
|
// Display Prompt
|
||||||
let input: String = Input::new()
|
let input: String = Input::new()
|
||||||
|
@ -735,21 +799,7 @@ async fn credential_update_exec(session_token: CUSessionToken, client: KanidmCli
|
||||||
.idm_account_credential_update_status(&session_token)
|
.idm_account_credential_update_status(&session_token)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(status) => {
|
Ok(status) => display_status(status),
|
||||||
if let Some(cred_detail) = status.primary {
|
|
||||||
println!("Primary Credential:");
|
|
||||||
print!("{}", cred_detail);
|
|
||||||
} else {
|
|
||||||
println!("Primary Credential:");
|
|
||||||
println!(" not set");
|
|
||||||
}
|
|
||||||
|
|
||||||
// We may need to be able to display if there are dangling
|
|
||||||
// curegstates, but the cli ui statemachine can match the
|
|
||||||
// server so it may not be needed?
|
|
||||||
|
|
||||||
println!("Can Commit: {}", status.can_commit);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("An error occured -> {:?}", e);
|
eprintln!("An error occured -> {:?}", e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,4 +25,4 @@ if [ -n "${1}" ]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
#shellcheck disable=SC2086
|
#shellcheck disable=SC2086
|
||||||
cargo run --bin kanidmd -- ${COMMAND} -c "${CONFIG_FILE}"
|
RUST_LOG=debug cargo run --bin kanidmd -- ${COMMAND} -c "${CONFIG_FILE}"
|
||||||
|
|
|
@ -983,7 +983,7 @@ impl QueryServerReadV1 {
|
||||||
let idms_cred_update = self.idms.cred_update_transaction_async().await;
|
let idms_cred_update = self.idms.cred_update_transaction_async().await;
|
||||||
let res = spanned!("actors::v1_read::handle<IdmCredentialUpdateStatus>", {
|
let res = spanned!("actors::v1_read::handle<IdmCredentialUpdateStatus>", {
|
||||||
let session_token = CredentialUpdateSessionToken {
|
let session_token = CredentialUpdateSessionToken {
|
||||||
token_enc: session_token.session_token,
|
token_enc: session_token.token,
|
||||||
};
|
};
|
||||||
|
|
||||||
idms_cred_update
|
idms_cred_update
|
||||||
|
@ -1016,7 +1016,7 @@ impl QueryServerReadV1 {
|
||||||
let idms_cred_update = self.idms.cred_update_transaction_async().await;
|
let idms_cred_update = self.idms.cred_update_transaction_async().await;
|
||||||
let res = spanned!("actors::v1_read::handle<IdmCredentialUpdate>", {
|
let res = spanned!("actors::v1_read::handle<IdmCredentialUpdate>", {
|
||||||
let session_token = CredentialUpdateSessionToken {
|
let session_token = CredentialUpdateSessionToken {
|
||||||
token_enc: session_token.session_token,
|
token_enc: session_token.token,
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!(?scr);
|
debug!(?scr);
|
||||||
|
|
|
@ -34,7 +34,7 @@ use kanidm_proto::v1::Entry as ProtoEntry;
|
||||||
use kanidm_proto::v1::Modify as ProtoModify;
|
use kanidm_proto::v1::Modify as ProtoModify;
|
||||||
use kanidm_proto::v1::ModifyList as ProtoModifyList;
|
use kanidm_proto::v1::ModifyList as ProtoModifyList;
|
||||||
use kanidm_proto::v1::{
|
use kanidm_proto::v1::{
|
||||||
AccountPersonSet, AccountUnixExtend, CUIntentToken, CUSessionToken, CreateRequest,
|
AccountPersonSet, AccountUnixExtend, CUIntentToken, CUSessionToken, CUStatus, CreateRequest,
|
||||||
DeleteRequest, GroupUnixExtend, ModifyRequest, SetCredentialRequest, SetCredentialResponse,
|
DeleteRequest, GroupUnixExtend, ModifyRequest, SetCredentialRequest, SetCredentialResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -667,7 +667,7 @@ impl QueryServerWriteV1 {
|
||||||
uat: Option<String>,
|
uat: Option<String>,
|
||||||
uuid_or_name: String,
|
uuid_or_name: String,
|
||||||
eventid: Uuid,
|
eventid: Uuid,
|
||||||
) -> Result<CUSessionToken, OperationError> {
|
) -> Result<(CUSessionToken, CUStatus), OperationError> {
|
||||||
let ct = duration_from_epoch_now();
|
let ct = duration_from_epoch_now();
|
||||||
let mut idms_prox_write = self.idms.proxy_write_async(ct).await;
|
let mut idms_prox_write = self.idms.proxy_write_async(ct).await;
|
||||||
let res = spanned!("actors::v1_write::handle<IdmCredentialUpdate>", {
|
let res = spanned!("actors::v1_write::handle<IdmCredentialUpdate>", {
|
||||||
|
@ -697,8 +697,13 @@ impl QueryServerWriteV1 {
|
||||||
);
|
);
|
||||||
e
|
e
|
||||||
})
|
})
|
||||||
.map(|tok| CUSessionToken {
|
.map(|(tok, sta)| {
|
||||||
session_token: tok.token_enc,
|
(
|
||||||
|
CUSessionToken {
|
||||||
|
token: tok.token_enc,
|
||||||
|
},
|
||||||
|
sta.into(),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
res
|
res
|
||||||
|
@ -750,7 +755,7 @@ impl QueryServerWriteV1 {
|
||||||
e
|
e
|
||||||
})
|
})
|
||||||
.map(|tok| CUIntentToken {
|
.map(|tok| CUIntentToken {
|
||||||
intent_token: tok.intent_id,
|
token: tok.intent_id,
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
res
|
res
|
||||||
|
@ -766,12 +771,12 @@ impl QueryServerWriteV1 {
|
||||||
&self,
|
&self,
|
||||||
intent_token: CUIntentToken,
|
intent_token: CUIntentToken,
|
||||||
eventid: Uuid,
|
eventid: Uuid,
|
||||||
) -> Result<CUSessionToken, OperationError> {
|
) -> Result<(CUSessionToken, CUStatus), OperationError> {
|
||||||
let ct = duration_from_epoch_now();
|
let ct = duration_from_epoch_now();
|
||||||
let mut idms_prox_write = self.idms.proxy_write_async(ct).await;
|
let mut idms_prox_write = self.idms.proxy_write_async(ct).await;
|
||||||
let res = spanned!("actors::v1_write::handle<IdmCredentialExchangeIntent>", {
|
let res = spanned!("actors::v1_write::handle<IdmCredentialExchangeIntent>", {
|
||||||
let intent_token = CredentialUpdateIntentToken {
|
let intent_token = CredentialUpdateIntentToken {
|
||||||
intent_id: intent_token.intent_token,
|
intent_id: intent_token.token,
|
||||||
};
|
};
|
||||||
|
|
||||||
idms_prox_write
|
idms_prox_write
|
||||||
|
@ -784,8 +789,13 @@ impl QueryServerWriteV1 {
|
||||||
);
|
);
|
||||||
e
|
e
|
||||||
})
|
})
|
||||||
.map(|tok| CUSessionToken {
|
.map(|(tok, sta)| {
|
||||||
session_token: tok.token_enc,
|
(
|
||||||
|
CUSessionToken {
|
||||||
|
token: tok.token_enc,
|
||||||
|
},
|
||||||
|
sta.into(),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
res
|
res
|
||||||
|
@ -806,7 +816,7 @@ impl QueryServerWriteV1 {
|
||||||
let mut idms_prox_write = self.idms.proxy_write_async(ct).await;
|
let mut idms_prox_write = self.idms.proxy_write_async(ct).await;
|
||||||
let res = spanned!("actors::v1_write::handle<IdmCredentialUpdateCommit>", {
|
let res = spanned!("actors::v1_write::handle<IdmCredentialUpdateCommit>", {
|
||||||
let session_token = CredentialUpdateSessionToken {
|
let session_token = CredentialUpdateSessionToken {
|
||||||
token_enc: session_token.session_token,
|
token_enc: session_token.token,
|
||||||
};
|
};
|
||||||
|
|
||||||
idms_prox_write
|
idms_prox_write
|
||||||
|
|
|
@ -57,7 +57,7 @@ enum MfaRegState {
|
||||||
None,
|
None,
|
||||||
TotpInit(Totp),
|
TotpInit(Totp),
|
||||||
TotpTryAgain(Totp),
|
TotpTryAgain(Totp),
|
||||||
TotpInvalidSha1(Totp),
|
TotpInvalidSha1(Totp, Totp),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for MfaRegState {
|
impl fmt::Debug for MfaRegState {
|
||||||
|
@ -66,7 +66,7 @@ impl fmt::Debug for MfaRegState {
|
||||||
MfaRegState::None => "MfaRegState::None",
|
MfaRegState::None => "MfaRegState::None",
|
||||||
MfaRegState::TotpInit(_) => "MfaRegState::TotpInit",
|
MfaRegState::TotpInit(_) => "MfaRegState::TotpInit",
|
||||||
MfaRegState::TotpTryAgain(_) => "MfaRegState::TotpTryAgain",
|
MfaRegState::TotpTryAgain(_) => "MfaRegState::TotpTryAgain",
|
||||||
MfaRegState::TotpInvalidSha1(_) => "MfaRegState::TotpInvalidSha1",
|
MfaRegState::TotpInvalidSha1(_, _) => "MfaRegState::TotpInvalidSha1",
|
||||||
};
|
};
|
||||||
write!(f, "{}", t)
|
write!(f, "{}", t)
|
||||||
}
|
}
|
||||||
|
@ -123,8 +123,9 @@ impl fmt::Debug for MfaRegStateStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct CredentialUpdateSessionStatus {
|
pub struct CredentialUpdateSessionStatus {
|
||||||
// spn: ?
|
spn: String,
|
||||||
|
displayname: String,
|
||||||
// ttl: Duration,
|
// ttl: Duration,
|
||||||
//
|
//
|
||||||
can_commit: bool,
|
can_commit: bool,
|
||||||
|
@ -136,6 +137,8 @@ pub(crate) struct CredentialUpdateSessionStatus {
|
||||||
impl Into<CUStatus> for CredentialUpdateSessionStatus {
|
impl Into<CUStatus> for CredentialUpdateSessionStatus {
|
||||||
fn into(self) -> CUStatus {
|
fn into(self) -> CUStatus {
|
||||||
CUStatus {
|
CUStatus {
|
||||||
|
spn: self.spn.clone(),
|
||||||
|
displayname: self.displayname.clone(),
|
||||||
can_commit: self.can_commit,
|
can_commit: self.can_commit,
|
||||||
primary: self.primary,
|
primary: self.primary,
|
||||||
mfaregstate: match self.mfaregstate {
|
mfaregstate: match self.mfaregstate {
|
||||||
|
@ -154,6 +157,9 @@ impl Into<CUStatus> for CredentialUpdateSessionStatus {
|
||||||
impl From<&CredentialUpdateSession> for CredentialUpdateSessionStatus {
|
impl From<&CredentialUpdateSession> for CredentialUpdateSessionStatus {
|
||||||
fn from(session: &CredentialUpdateSession) -> Self {
|
fn from(session: &CredentialUpdateSession) -> Self {
|
||||||
CredentialUpdateSessionStatus {
|
CredentialUpdateSessionStatus {
|
||||||
|
spn: session.account.spn.clone(),
|
||||||
|
displayname: session.account.displayname.clone(),
|
||||||
|
|
||||||
can_commit: true,
|
can_commit: true,
|
||||||
primary: session.primary.as_ref().map(|c| c.into()),
|
primary: session.primary.as_ref().map(|c| c.into()),
|
||||||
mfaregstate: match &session.mfaregstate {
|
mfaregstate: match &session.mfaregstate {
|
||||||
|
@ -162,7 +168,7 @@ impl From<&CredentialUpdateSession> for CredentialUpdateSessionStatus {
|
||||||
token.to_proto(session.account.name.as_str(), session.account.spn.as_str()),
|
token.to_proto(session.account.name.as_str(), session.account.spn.as_str()),
|
||||||
),
|
),
|
||||||
MfaRegState::TotpTryAgain(_) => MfaRegStateStatus::TotpTryAgain,
|
MfaRegState::TotpTryAgain(_) => MfaRegStateStatus::TotpTryAgain,
|
||||||
MfaRegState::TotpInvalidSha1(_) => MfaRegStateStatus::TotpInvalidSha1,
|
MfaRegState::TotpInvalidSha1(_, _) => MfaRegStateStatus::TotpInvalidSha1,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -284,17 +290,20 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
intent_token_id: Option<String>,
|
intent_token_id: Option<String>,
|
||||||
account: Account,
|
account: Account,
|
||||||
ct: Duration,
|
ct: Duration,
|
||||||
) -> Result<CredentialUpdateSessionToken, OperationError> {
|
) -> Result<(CredentialUpdateSessionToken, CredentialUpdateSessionStatus), OperationError> {
|
||||||
// - stash the current state of all associated credentials
|
// - stash the current state of all associated credentials
|
||||||
let primary = account.primary.clone();
|
let primary = account.primary.clone();
|
||||||
// - store account policy (if present)
|
// - store account policy (if present)
|
||||||
|
let session = CredentialUpdateSession {
|
||||||
let session = Arc::new(Mutex::new(CredentialUpdateSession {
|
|
||||||
account,
|
account,
|
||||||
intent_token_id,
|
intent_token_id,
|
||||||
primary,
|
primary,
|
||||||
mfaregstate: MfaRegState::None,
|
mfaregstate: MfaRegState::None,
|
||||||
}));
|
};
|
||||||
|
|
||||||
|
let status: CredentialUpdateSessionStatus = (&session).into();
|
||||||
|
|
||||||
|
let session = Arc::new(Mutex::new(session));
|
||||||
|
|
||||||
let max_ttl = ct + MAXIMUM_CRED_UPDATE_TTL;
|
let max_ttl = ct + MAXIMUM_CRED_UPDATE_TTL;
|
||||||
|
|
||||||
|
@ -317,7 +326,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
trace!("cred_update_sessions.insert - {}", sessionid);
|
trace!("cred_update_sessions.insert - {}", sessionid);
|
||||||
|
|
||||||
// - issue the CredentialUpdateToken (enc)
|
// - issue the CredentialUpdateToken (enc)
|
||||||
Ok(CredentialUpdateSessionToken { token_enc })
|
Ok((CredentialUpdateSessionToken { token_enc }, status))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_credential_update_intent(
|
pub fn init_credential_update_intent(
|
||||||
|
@ -405,7 +414,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
&mut self,
|
&mut self,
|
||||||
token: CredentialUpdateIntentToken,
|
token: CredentialUpdateIntentToken,
|
||||||
ct: Duration,
|
ct: Duration,
|
||||||
) -> Result<CredentialUpdateSessionToken, OperationError> {
|
) -> Result<(CredentialUpdateSessionToken, CredentialUpdateSessionStatus), OperationError> {
|
||||||
let CredentialUpdateIntentToken { intent_id } = token;
|
let CredentialUpdateIntentToken { intent_id } = token;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -582,7 +591,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
&mut self,
|
&mut self,
|
||||||
event: &InitCredentialUpdateEvent,
|
event: &InitCredentialUpdateEvent,
|
||||||
ct: Duration,
|
ct: Duration,
|
||||||
) -> Result<CredentialUpdateSessionToken, OperationError> {
|
) -> Result<(CredentialUpdateSessionToken, CredentialUpdateSessionStatus), OperationError> {
|
||||||
spanned!("idm::server::credupdatesession<Init>", {
|
spanned!("idm::server::credupdatesession<Init>", {
|
||||||
let account = self.validate_init_credential_update(event.target, &event.ident)?;
|
let account = self.validate_init_credential_update(event.target, &event.ident)?;
|
||||||
|
|
||||||
|
@ -1004,7 +1013,9 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
|
||||||
|
|
||||||
// Are we in a totp reg state?
|
// Are we in a totp reg state?
|
||||||
match &session.mfaregstate {
|
match &session.mfaregstate {
|
||||||
MfaRegState::TotpInit(totp_token) | MfaRegState::TotpTryAgain(totp_token) => {
|
MfaRegState::TotpInit(totp_token)
|
||||||
|
| MfaRegState::TotpTryAgain(totp_token)
|
||||||
|
| MfaRegState::TotpInvalidSha1(totp_token, _) => {
|
||||||
if totp_token.verify(totp_chal, &ct) {
|
if totp_token.verify(totp_chal, &ct) {
|
||||||
// It was valid. Update the credential.
|
// It was valid. Update the credential.
|
||||||
let ncred = session
|
let ncred = session
|
||||||
|
@ -1030,7 +1041,8 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
|
||||||
if token_sha1.verify(totp_chal, &ct) {
|
if token_sha1.verify(totp_chal, &ct) {
|
||||||
// Greeeaaaaaatttt it's a broken app. Let's check the user
|
// Greeeaaaaaatttt it's a broken app. Let's check the user
|
||||||
// knows this is broken, before we proceed.
|
// knows this is broken, before we proceed.
|
||||||
session.mfaregstate = MfaRegState::TotpInvalidSha1(token_sha1);
|
session.mfaregstate =
|
||||||
|
MfaRegState::TotpInvalidSha1(totp_token.clone(), token_sha1);
|
||||||
Ok(session.deref().into())
|
Ok(session.deref().into())
|
||||||
} else {
|
} else {
|
||||||
// Let them check again, it's a typo.
|
// Let them check again, it's a typo.
|
||||||
|
@ -1057,7 +1069,7 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
|
||||||
|
|
||||||
// Are we in a totp reg state?
|
// Are we in a totp reg state?
|
||||||
match &session.mfaregstate {
|
match &session.mfaregstate {
|
||||||
MfaRegState::TotpInvalidSha1(token_sha1) => {
|
MfaRegState::TotpInvalidSha1(_, token_sha1) => {
|
||||||
// They have accepted it as sha1
|
// They have accepted it as sha1
|
||||||
let ncred = session
|
let ncred = session
|
||||||
.primary
|
.primary
|
||||||
|
@ -1222,8 +1234,9 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
CredentialUpdateSessionToken, InitCredentialUpdateEvent, InitCredentialUpdateIntentEvent,
|
CredentialUpdateSessionStatus, CredentialUpdateSessionToken, InitCredentialUpdateEvent,
|
||||||
MfaRegStateStatus, MAXIMUM_CRED_UPDATE_TTL, MAXIMUM_INTENT_TTL, MINIMUM_INTENT_TTL,
|
InitCredentialUpdateIntentEvent, MfaRegStateStatus, MAXIMUM_CRED_UPDATE_TTL,
|
||||||
|
MAXIMUM_INTENT_TTL, MINIMUM_INTENT_TTL,
|
||||||
};
|
};
|
||||||
use crate::credential::totp::Totp;
|
use crate::credential::totp::Totp;
|
||||||
use crate::event::{AuthEvent, AuthResult, CreateEvent};
|
use crate::event::{AuthEvent, AuthResult, CreateEvent};
|
||||||
|
@ -1348,7 +1361,10 @@ mod tests {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_test_session(idms: &IdmServer, ct: Duration) -> CredentialUpdateSessionToken {
|
fn setup_test_session(
|
||||||
|
idms: &IdmServer,
|
||||||
|
ct: Duration,
|
||||||
|
) -> (CredentialUpdateSessionToken, CredentialUpdateSessionStatus) {
|
||||||
let mut idms_prox_write = idms.proxy_write(ct);
|
let mut idms_prox_write = idms.proxy_write(ct);
|
||||||
|
|
||||||
let e2 = entry_init!(
|
let e2 = entry_init!(
|
||||||
|
@ -1380,7 +1396,10 @@ mod tests {
|
||||||
cur.expect("Failed to start update")
|
cur.expect("Failed to start update")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn renew_test_session(idms: &IdmServer, ct: Duration) -> CredentialUpdateSessionToken {
|
fn renew_test_session(
|
||||||
|
idms: &IdmServer,
|
||||||
|
ct: Duration,
|
||||||
|
) -> (CredentialUpdateSessionToken, CredentialUpdateSessionStatus) {
|
||||||
let mut idms_prox_write = idms.proxy_write(ct);
|
let mut idms_prox_write = idms.proxy_write(ct);
|
||||||
|
|
||||||
let testperson = idms_prox_write
|
let testperson = idms_prox_write
|
||||||
|
@ -1591,7 +1610,7 @@ mod tests {
|
||||||
idms: &IdmServer,
|
idms: &IdmServer,
|
||||||
_idms_delayed: &mut IdmServerDelayed| {
|
_idms_delayed: &mut IdmServerDelayed| {
|
||||||
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||||
let cust = setup_test_session(idms, ct);
|
let (cust, _) = setup_test_session(idms, ct);
|
||||||
|
|
||||||
let cutxn = idms.cred_update_transaction();
|
let cutxn = idms.cred_update_transaction();
|
||||||
// The session exists
|
// The session exists
|
||||||
|
@ -1621,7 +1640,7 @@ mod tests {
|
||||||
let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
|
let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
|
||||||
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||||
|
|
||||||
let cust = setup_test_session(idms, ct);
|
let (cust, _) = setup_test_session(idms, ct);
|
||||||
|
|
||||||
let cutxn = idms.cred_update_transaction();
|
let cutxn = idms.cred_update_transaction();
|
||||||
|
|
||||||
|
@ -1651,7 +1670,7 @@ mod tests {
|
||||||
assert!(check_testperson_password(idms, test_pw, ct).is_some());
|
assert!(check_testperson_password(idms, test_pw, ct).is_some());
|
||||||
|
|
||||||
// Test deleting the pw
|
// Test deleting the pw
|
||||||
let cust = renew_test_session(idms, ct);
|
let (cust, _) = renew_test_session(idms, ct);
|
||||||
let cutxn = idms.cred_update_transaction();
|
let cutxn = idms.cred_update_transaction();
|
||||||
|
|
||||||
let c_status = cutxn
|
let c_status = cutxn
|
||||||
|
@ -1687,7 +1706,7 @@ mod tests {
|
||||||
let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
|
let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
|
||||||
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||||
|
|
||||||
let cust = setup_test_session(idms, ct);
|
let (cust, _) = setup_test_session(idms, ct);
|
||||||
let cutxn = idms.cred_update_transaction();
|
let cutxn = idms.cred_update_transaction();
|
||||||
|
|
||||||
// Setup the PW
|
// Setup the PW
|
||||||
|
@ -1746,7 +1765,7 @@ mod tests {
|
||||||
// No need to test delete of the whole cred, we already did with pw above.
|
// No need to test delete of the whole cred, we already did with pw above.
|
||||||
|
|
||||||
// If we remove TOTP, show it reverts back.
|
// If we remove TOTP, show it reverts back.
|
||||||
let cust = renew_test_session(idms, ct);
|
let (cust, _) = renew_test_session(idms, ct);
|
||||||
let cutxn = idms.cred_update_transaction();
|
let cutxn = idms.cred_update_transaction();
|
||||||
|
|
||||||
let c_status = cutxn
|
let c_status = cutxn
|
||||||
|
@ -1776,7 +1795,7 @@ mod tests {
|
||||||
let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
|
let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
|
||||||
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||||
|
|
||||||
let cust = setup_test_session(idms, ct);
|
let (cust, _) = setup_test_session(idms, ct);
|
||||||
let cutxn = idms.cred_update_transaction();
|
let cutxn = idms.cred_update_transaction();
|
||||||
|
|
||||||
// Setup the PW
|
// Setup the PW
|
||||||
|
@ -1847,7 +1866,7 @@ mod tests {
|
||||||
"fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
|
"fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
|
||||||
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||||
|
|
||||||
let cust = setup_test_session(idms, ct);
|
let (cust, _) = setup_test_session(idms, ct);
|
||||||
let cutxn = idms.cred_update_transaction();
|
let cutxn = idms.cred_update_transaction();
|
||||||
|
|
||||||
// Setup the PW
|
// Setup the PW
|
||||||
|
@ -1923,7 +1942,7 @@ mod tests {
|
||||||
assert!(r.is_ok());
|
assert!(r.is_ok());
|
||||||
|
|
||||||
// Renew to start the next steps
|
// Renew to start the next steps
|
||||||
let cust = renew_test_session(idms, ct);
|
let (cust, _) = renew_test_session(idms, ct);
|
||||||
let cutxn = idms.cred_update_transaction();
|
let cutxn = idms.cred_update_transaction();
|
||||||
|
|
||||||
// Only 7 codes left.
|
// Only 7 codes left.
|
||||||
|
@ -1986,7 +2005,7 @@ mod tests {
|
||||||
let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
|
let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
|
||||||
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||||
|
|
||||||
let cust = setup_test_session(idms, ct);
|
let (cust, _) = setup_test_session(idms, ct);
|
||||||
let cutxn = idms.cred_update_transaction();
|
let cutxn = idms.cred_update_transaction();
|
||||||
|
|
||||||
// Setup the PW
|
// Setup the PW
|
||||||
|
|
|
@ -133,7 +133,9 @@ pub fn to_tide_response<T: Serialize>(
|
||||||
OperationError::NoMatchingEntries => {
|
OperationError::NoMatchingEntries => {
|
||||||
tide::Response::new(tide::StatusCode::NotFound)
|
tide::Response::new(tide::StatusCode::NotFound)
|
||||||
}
|
}
|
||||||
OperationError::EmptyRequest | OperationError::SchemaViolation(_) => {
|
OperationError::PasswordQuality(_)
|
||||||
|
| OperationError::EmptyRequest
|
||||||
|
| OperationError::SchemaViolation(_) => {
|
||||||
tide::Response::new(tide::StatusCode::BadRequest)
|
tide::Response::new(tide::StatusCode::BadRequest)
|
||||||
}
|
}
|
||||||
_ => tide::Response::new(tide::StatusCode::InternalServerError),
|
_ => tide::Response::new(tide::StatusCode::InternalServerError),
|
||||||
|
|
|
@ -1394,7 +1394,7 @@ async fn test_server_credential_update_session_pw() {
|
||||||
// Logout, we don't need any auth now.
|
// Logout, we don't need any auth now.
|
||||||
let _ = rsclient.logout();
|
let _ = rsclient.logout();
|
||||||
// Exchange the intent token
|
// Exchange the intent token
|
||||||
let session_token = rsclient
|
let (session_token, _status) = rsclient
|
||||||
.idm_account_credential_update_exchange(intent_token)
|
.idm_account_credential_update_exchange(intent_token)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -1458,7 +1458,7 @@ async fn test_server_credential_update_session_totp_pw() {
|
||||||
// Logout, we don't need any auth now, the intent tokens care for it.
|
// Logout, we don't need any auth now, the intent tokens care for it.
|
||||||
let _ = rsclient.logout();
|
let _ = rsclient.logout();
|
||||||
// Exchange the intent token
|
// Exchange the intent token
|
||||||
let session_token = rsclient
|
let (session_token, _statu) = rsclient
|
||||||
.idm_account_credential_update_exchange(intent_token)
|
.idm_account_credential_update_exchange(intent_token)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -1523,7 +1523,7 @@ async fn test_server_credential_update_session_totp_pw() {
|
||||||
// We are now authed as the demo_account
|
// We are now authed as the demo_account
|
||||||
|
|
||||||
// Self create the session and remove the totp now.
|
// Self create the session and remove the totp now.
|
||||||
let session_token = rsclient
|
let (session_token, _status) = rsclient
|
||||||
.idm_account_credential_update_begin("demo_account")
|
.idm_account_credential_update_begin("demo_account")
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -25,12 +25,17 @@ wasm-bindgen-futures = { version = "^0.4.30" }
|
||||||
kanidm_proto = { path = "../kanidm_proto" }
|
kanidm_proto = { path = "../kanidm_proto" }
|
||||||
|
|
||||||
webauthn-rs = { version = "^0.3.2", default-features = false, features = ["wasm"] }
|
webauthn-rs = { version = "^0.3.2", default-features = false, features = ["wasm"] }
|
||||||
|
qrcode = { version = "^0.12.0", default-features = false, features = ["svg"] }
|
||||||
|
|
||||||
yew-router = "^0.16.0"
|
yew-router = "^0.16.0"
|
||||||
yew = "^0.19.3"
|
yew = "^0.19.3"
|
||||||
|
yew-agent = "^0.1.0"
|
||||||
gloo = "^0.7.0"
|
gloo = "^0.7.0"
|
||||||
js-sys = "^0.3.57"
|
js-sys = "^0.3.57"
|
||||||
|
|
||||||
|
compact_jwt = { version = "^0.2.2", default-features = false, features = ["unsafe_release_without_verify"] }
|
||||||
|
# compact_jwt = { path = "../../compact_jwt" , default-features = false, features = ["unsafe_release_without_verify"] }
|
||||||
|
|
||||||
[dependencies.web-sys]
|
[dependencies.web-sys]
|
||||||
version = "^0.3.57"
|
version = "^0.3.57"
|
||||||
features = [
|
features = [
|
||||||
|
@ -44,6 +49,7 @@ features = [
|
||||||
"Event",
|
"Event",
|
||||||
"FocusEvent",
|
"FocusEvent",
|
||||||
"Headers",
|
"Headers",
|
||||||
|
"HtmlButtonElement",
|
||||||
"HtmlDocument",
|
"HtmlDocument",
|
||||||
"Navigator",
|
"Navigator",
|
||||||
"PublicKeyCredential",
|
"PublicKeyCredential",
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
wasm-pack build --dev --target web && \
|
wasm-pack build --dev --target web && \
|
||||||
|
touch ./pkg/ANYTHING_HERE_WILL_BE_DELETED_ADD_TO_SRC && \
|
||||||
cp ./src/style.css ./pkg/style.css && \
|
cp ./src/style.css ./pkg/style.css && \
|
||||||
|
cp ./src/wasmloader.js ./pkg/wasmloader.js && \
|
||||||
|
cp ./src/favicon.svg ./pkg/favicon.svg && \
|
||||||
cp -a ./src/external ./pkg/external && \
|
cp -a ./src/external ./pkg/external && \
|
||||||
rm ./pkg/.gitignore
|
rm ./pkg/.gitignore
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
wasm-pack build --no-typescript --release --target web && \
|
wasm-pack build --release --target web && \
|
||||||
|
touch ./pkg/ANYTHING_HERE_WILL_BE_DELETED_ADD_TO_SRC && \
|
||||||
cp ./src/style.css ./pkg/style.css && \
|
cp ./src/style.css ./pkg/style.css && \
|
||||||
|
cp ./src/wasmloader.js ./pkg/wasmloader.js && \
|
||||||
|
cp ./src/favicon.svg ./pkg/favicon.svg && \
|
||||||
cp -a ./src/external ./pkg/external && \
|
cp -a ./src/external ./pkg/external && \
|
||||||
rm ./pkg/.gitignore
|
rm ./pkg/.gitignore
|
||||||
|
|
||||||
|
|
|
@ -66,10 +66,10 @@ all backgrounds.
|
||||||
* CLI for administration
|
* CLI for administration
|
||||||
* WebUI for self-service with wifi enrollment, claim management and more.
|
* WebUI for self-service with wifi enrollment, claim management and more.
|
||||||
* RBAC/Claims/Policy (limited by time and credential scope)
|
* RBAC/Claims/Policy (limited by time and credential scope)
|
||||||
|
* OIDC/Oauth
|
||||||
|
|
||||||
### Upcoming Focus Areas
|
### Upcoming Focus Areas
|
||||||
|
|
||||||
* OIDC/Oauth
|
|
||||||
* Replication (async multiple active write servers, read-only servers)
|
* Replication (async multiple active write servers, read-only servers)
|
||||||
|
|
||||||
### Future
|
### Future
|
||||||
|
|
31
kanidmd_web_ui/pkg/kanidmd_web_ui.d.ts
vendored
Normal file
31
kanidmd_web_ui/pkg/kanidmd_web_ui.d.ts
vendored
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
*/
|
||||||
|
export function run_app(): void;
|
||||||
|
|
||||||
|
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||||
|
|
||||||
|
export interface InitOutput {
|
||||||
|
readonly memory: WebAssembly.Memory;
|
||||||
|
readonly run_app: (a: number) => void;
|
||||||
|
readonly __wbindgen_malloc: (a: number) => number;
|
||||||
|
readonly __wbindgen_realloc: (a: number, b: number, c: number) => number;
|
||||||
|
readonly __wbindgen_export_2: WebAssembly.Table;
|
||||||
|
readonly _dyn_core__ops__function__Fn__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__ha6256632cf9b6e15: (a: number, b: number, c: number) => void;
|
||||||
|
readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h026107843485189d: (a: number, b: number, c: number) => void;
|
||||||
|
readonly _dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hf1579e791c670fad: (a: number, b: number, c: number) => void;
|
||||||
|
readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
|
||||||
|
readonly __wbindgen_free: (a: number, b: number) => void;
|
||||||
|
readonly __wbindgen_exn_store: (a: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
|
||||||
|
* for everything else, calls `WebAssembly.instantiate` directly.
|
||||||
|
*
|
||||||
|
* @param {InitInput | Promise<InitInput>} module_or_path
|
||||||
|
*
|
||||||
|
* @returns {Promise<InitOutput>}
|
||||||
|
*/
|
||||||
|
export default function init (module_or_path?: InitInput | Promise<InitInput>): Promise<InitOutput>;
|
File diff suppressed because it is too large
Load diff
Binary file not shown.
13
kanidmd_web_ui/pkg/kanidmd_web_ui_bg.wasm.d.ts
vendored
Normal file
13
kanidmd_web_ui/pkg/kanidmd_web_ui_bg.wasm.d.ts
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export const memory: WebAssembly.Memory;
|
||||||
|
export function run_app(a: number): void;
|
||||||
|
export function __wbindgen_malloc(a: number): number;
|
||||||
|
export function __wbindgen_realloc(a: number, b: number, c: number): number;
|
||||||
|
export const __wbindgen_export_2: WebAssembly.Table;
|
||||||
|
export function _dyn_core__ops__function__Fn__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__ha6256632cf9b6e15(a: number, b: number, c: number): void;
|
||||||
|
export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h026107843485189d(a: number, b: number, c: number): void;
|
||||||
|
export function _dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hf1579e791c670fad(a: number, b: number, c: number): void;
|
||||||
|
export function __wbindgen_add_to_stack_pointer(a: number): number;
|
||||||
|
export function __wbindgen_free(a: number, b: number): void;
|
||||||
|
export function __wbindgen_exn_store(a: number): void;
|
|
@ -5,7 +5,7 @@
|
||||||
"James Hodgkinson <james@terminaloutcomes.com>"
|
"James Hodgkinson <james@terminaloutcomes.com>"
|
||||||
],
|
],
|
||||||
"description": "Kanidm Server Web User Interface",
|
"description": "Kanidm Server Web User Interface",
|
||||||
"version": "1.1.0-alpha.7",
|
"version": "1.1.0-alpha.8",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -14,9 +14,11 @@
|
||||||
"files": [
|
"files": [
|
||||||
"kanidmd_web_ui_bg.wasm",
|
"kanidmd_web_ui_bg.wasm",
|
||||||
"kanidmd_web_ui.js",
|
"kanidmd_web_ui.js",
|
||||||
|
"kanidmd_web_ui.d.ts",
|
||||||
"LICENSE.md"
|
"LICENSE.md"
|
||||||
],
|
],
|
||||||
"module": "kanidmd_web_ui.js",
|
"module": "kanidmd_web_ui.js",
|
||||||
"homepage": "https://github.com/kanidm/kanidm/",
|
"homepage": "https://github.com/kanidm/kanidm/",
|
||||||
|
"types": "kanidmd_web_ui.d.ts",
|
||||||
"sideEffects": false
|
"sideEffects": false
|
||||||
}
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
export function modal_hide(m) {
|
||||||
|
var elem = document.getElementById(m);
|
||||||
|
var modal = bootstrap.Modal.getInstance(elem);
|
||||||
|
modal.hide();
|
||||||
|
}
|
|
@ -3,6 +3,13 @@ body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-cred-reset-body {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
padding: 15px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.form-signin-body {
|
.form-signin-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -140,3 +147,10 @@ body {
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
|
box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vert-center {
|
||||||
|
height: 80%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
197
kanidmd_web_ui/src/credential/delete.rs
Normal file
197
kanidmd_web_ui/src/credential/delete.rs
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
use crate::error::*;
|
||||||
|
use crate::utils;
|
||||||
|
|
||||||
|
use super::eventbus::{EventBus, EventBusMsg};
|
||||||
|
use super::reset::ModalProps;
|
||||||
|
|
||||||
|
use gloo::console;
|
||||||
|
use web_sys::Node;
|
||||||
|
use yew::prelude::*;
|
||||||
|
use yew_agent::{Dispatched, Dispatcher};
|
||||||
|
|
||||||
|
use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt};
|
||||||
|
use wasm_bindgen_futures::JsFuture;
|
||||||
|
use web_sys::{Request, RequestInit, RequestMode, Response};
|
||||||
|
|
||||||
|
use kanidm_proto::v1::{
|
||||||
|
CURegState, CURequest, CUSessionToken, CUStatus, OperationError, PasswordFeedback, TotpSecret,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum State {
|
||||||
|
Init,
|
||||||
|
Waiting,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DeleteApp {
|
||||||
|
state: State,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Msg {
|
||||||
|
Cancel,
|
||||||
|
Submit,
|
||||||
|
Error { emsg: String, kopid: Option<String> },
|
||||||
|
Success,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FetchError> for Msg {
|
||||||
|
fn from(fe: FetchError) -> Self {
|
||||||
|
Msg::Error {
|
||||||
|
emsg: fe.as_string(),
|
||||||
|
kopid: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeleteApp {
|
||||||
|
fn reset_and_hide(&mut self) {
|
||||||
|
utils::modal_hide_by_id("staticDeletePrimaryCred");
|
||||||
|
self.state = State::Init;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn submit_update(token: CUSessionToken, req: CURequest) -> Result<Msg, FetchError> {
|
||||||
|
let req_jsvalue = serde_json::to_string(&(req, token))
|
||||||
|
.map(|s| JsValue::from(&s))
|
||||||
|
.expect_throw("Failed to serialise pw curequest");
|
||||||
|
|
||||||
|
let mut opts = RequestInit::new();
|
||||||
|
opts.method("POST");
|
||||||
|
opts.mode(RequestMode::SameOrigin);
|
||||||
|
|
||||||
|
opts.body(Some(&req_jsvalue));
|
||||||
|
|
||||||
|
let request = Request::new_with_str_and_init("/v1/credential/_update", &opts)?;
|
||||||
|
request
|
||||||
|
.headers()
|
||||||
|
.set("content-type", "application/json")
|
||||||
|
.expect_throw("failed to set header");
|
||||||
|
|
||||||
|
let window = utils::window();
|
||||||
|
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
|
||||||
|
let resp: Response = resp_value.dyn_into().expect_throw("Invalid response type");
|
||||||
|
let status = resp.status();
|
||||||
|
let headers = resp.headers();
|
||||||
|
|
||||||
|
let kopid = headers.get("x-kanidm-opid").ok().flatten();
|
||||||
|
|
||||||
|
if status == 200 {
|
||||||
|
let jsval = JsFuture::from(resp.json()?).await?;
|
||||||
|
let status: CUStatus = jsval.into_serde().expect_throw("Invalid response type");
|
||||||
|
|
||||||
|
EventBus::dispatcher().send(EventBusMsg::UpdateStatus {
|
||||||
|
status: status.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Msg::Success)
|
||||||
|
} else {
|
||||||
|
let text = JsFuture::from(resp.text()?).await?;
|
||||||
|
let emsg = text.as_string().unwrap_or_else(|| "".to_string());
|
||||||
|
Ok(Msg::Error { emsg, kopid })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for DeleteApp {
|
||||||
|
type Message = Msg;
|
||||||
|
type Properties = ModalProps;
|
||||||
|
|
||||||
|
fn create(ctx: &Context<Self>) -> Self {
|
||||||
|
console::log!("delete modal create");
|
||||||
|
|
||||||
|
DeleteApp { state: State::Init }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn changed(&mut self, _ctx: &Context<Self>) -> bool {
|
||||||
|
console::log!("delete modal::change");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||||
|
console::log!("delete modal::update");
|
||||||
|
let token_c = ctx.props().token.clone();
|
||||||
|
match msg {
|
||||||
|
Msg::Cancel => {
|
||||||
|
self.reset_and_hide();
|
||||||
|
}
|
||||||
|
Msg::Submit => {
|
||||||
|
ctx.link().send_future(async {
|
||||||
|
match Self::submit_update(token_c, CURequest::PrimaryRemove).await {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(v) => v.into(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.state = State::Waiting;
|
||||||
|
}
|
||||||
|
Msg::Success => {
|
||||||
|
self.reset_and_hide();
|
||||||
|
}
|
||||||
|
Msg::Error { emsg, kopid } => {
|
||||||
|
// Submit the error to the parent.
|
||||||
|
EventBus::dispatcher().send(EventBusMsg::Error { emsg, kopid });
|
||||||
|
self.reset_and_hide();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
|
||||||
|
console::log!("delete modal::rendered");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destroy(&mut self, _ctx: &Context<Self>) {
|
||||||
|
console::log!("delete modal::destroy");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
|
console::log!("delete modal::view");
|
||||||
|
|
||||||
|
let submit_enabled = match &self.state {
|
||||||
|
State::Init => true,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div class="modal fade" id="staticDeletePrimaryCred" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="staticDeletePrimaryCred" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="staticDeletePrimaryCredLabel">{ "Delete Credential" }</h5>
|
||||||
|
<button type="button" class="btn-close" aria-label="Close"
|
||||||
|
onclick={
|
||||||
|
ctx.link()
|
||||||
|
.callback(move |_| {
|
||||||
|
Msg::Cancel
|
||||||
|
})
|
||||||
|
}
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
|
||||||
|
<p>{ "Delete your Password and any associated MFA?" }</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button id="delete-cancel" type="button" class="btn btn-secondary"
|
||||||
|
onclick={
|
||||||
|
ctx.link()
|
||||||
|
.callback(move |_| {
|
||||||
|
Msg::Cancel
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>{ "Cancel" }</button>
|
||||||
|
<button id="delete-submit" type="button" class="btn btn-danger"
|
||||||
|
disabled={ !submit_enabled }
|
||||||
|
onclick={
|
||||||
|
ctx.link()
|
||||||
|
.callback(move |_| {
|
||||||
|
Msg::Submit
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>{ "Submit" }</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
47
kanidmd_web_ui/src/credential/eventbus.rs
Normal file
47
kanidmd_web_ui/src/credential/eventbus.rs
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use yew_agent::{Agent, AgentLink, Context, HandlerId};
|
||||||
|
|
||||||
|
use kanidm_proto::v1::CUStatus;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub enum EventBusMsg {
|
||||||
|
UpdateStatus { status: CUStatus },
|
||||||
|
Error { emsg: String, kopid: Option<String> },
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EventBus {
|
||||||
|
link: AgentLink<EventBus>,
|
||||||
|
subscribers: HashSet<HandlerId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Agent for EventBus {
|
||||||
|
type Reach = Context<Self>;
|
||||||
|
type Message = ();
|
||||||
|
type Input = EventBusMsg;
|
||||||
|
type Output = EventBusMsg;
|
||||||
|
|
||||||
|
fn create(link: AgentLink<Self>) -> Self {
|
||||||
|
Self {
|
||||||
|
link,
|
||||||
|
subscribers: HashSet::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, _msg: Self::Message) {}
|
||||||
|
|
||||||
|
fn handle_input(&mut self, msg: Self::Input, _id: HandlerId) {
|
||||||
|
for sub in self.subscribers.iter() {
|
||||||
|
self.link.respond(*sub, msg.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn connected(&mut self, id: HandlerId) {
|
||||||
|
self.subscribers.insert(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn disconnected(&mut self, id: HandlerId) {
|
||||||
|
self.subscribers.remove(&id);
|
||||||
|
}
|
||||||
|
}
|
6
kanidmd_web_ui/src/credential/mod.rs
Normal file
6
kanidmd_web_ui/src/credential/mod.rs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
pub mod reset;
|
||||||
|
|
||||||
|
mod delete;
|
||||||
|
mod eventbus;
|
||||||
|
mod pwmodal;
|
||||||
|
mod totpmodal;
|
321
kanidmd_web_ui/src/credential/pwmodal.rs
Normal file
321
kanidmd_web_ui/src/credential/pwmodal.rs
Normal file
|
@ -0,0 +1,321 @@
|
||||||
|
use crate::error::*;
|
||||||
|
use crate::utils;
|
||||||
|
|
||||||
|
use super::eventbus::{EventBus, EventBusMsg};
|
||||||
|
use super::reset::ModalProps;
|
||||||
|
|
||||||
|
use gloo::console;
|
||||||
|
use yew::prelude::*;
|
||||||
|
use yew_agent::{Dispatched, Dispatcher};
|
||||||
|
|
||||||
|
use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt};
|
||||||
|
use wasm_bindgen_futures::JsFuture;
|
||||||
|
use web_sys::{Request, RequestInit, RequestMode, Response};
|
||||||
|
|
||||||
|
use kanidm_proto::v1::{CURequest, CUSessionToken, CUStatus, OperationError, PasswordFeedback};
|
||||||
|
|
||||||
|
enum PwState {
|
||||||
|
Init,
|
||||||
|
Feedback(Vec<PasswordFeedback>),
|
||||||
|
Waiting,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PwCheck {
|
||||||
|
Init,
|
||||||
|
Valid,
|
||||||
|
Invalid,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PwModalApp {
|
||||||
|
state: PwState,
|
||||||
|
pw_check: PwCheck,
|
||||||
|
pw_val: String,
|
||||||
|
pw_check_val: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Msg {
|
||||||
|
PasswordCheck,
|
||||||
|
PasswordSubmit,
|
||||||
|
PasswordCancel,
|
||||||
|
PasswordResponseQuality { feedback: Vec<PasswordFeedback> },
|
||||||
|
PasswordResponseSuccess { status: CUStatus },
|
||||||
|
Error { emsg: String, kopid: Option<String> },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FetchError> for Msg {
|
||||||
|
fn from(fe: FetchError) -> Self {
|
||||||
|
Msg::Error {
|
||||||
|
emsg: fe.as_string(),
|
||||||
|
kopid: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PwModalApp {
|
||||||
|
fn reset_and_hide(&mut self) {
|
||||||
|
utils::modal_hide_by_id("staticPassword");
|
||||||
|
self.pw_val = "".to_string();
|
||||||
|
self.pw_check_val = "".to_string();
|
||||||
|
self.pw_check = PwCheck::Init;
|
||||||
|
self.state = PwState::Init;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn submit_password_update(token: CUSessionToken, pw: String) -> Result<Msg, FetchError> {
|
||||||
|
let intentreq_jsvalue = serde_json::to_string(&(CURequest::Password(pw), token))
|
||||||
|
.map(|s| JsValue::from(&s))
|
||||||
|
.expect_throw("Failed to serialise pw curequest");
|
||||||
|
|
||||||
|
let mut opts = RequestInit::new();
|
||||||
|
opts.method("POST");
|
||||||
|
opts.mode(RequestMode::SameOrigin);
|
||||||
|
|
||||||
|
opts.body(Some(&intentreq_jsvalue));
|
||||||
|
|
||||||
|
let request = Request::new_with_str_and_init("/v1/credential/_update", &opts)?;
|
||||||
|
request
|
||||||
|
.headers()
|
||||||
|
.set("content-type", "application/json")
|
||||||
|
.expect_throw("failed to set header");
|
||||||
|
|
||||||
|
let window = utils::window();
|
||||||
|
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
|
||||||
|
let resp: Response = resp_value.dyn_into().expect_throw("Invalid response type");
|
||||||
|
let status = resp.status();
|
||||||
|
let headers = resp.headers();
|
||||||
|
|
||||||
|
if status == 200 {
|
||||||
|
let jsval = JsFuture::from(resp.json()?).await?;
|
||||||
|
let status: CUStatus = jsval.into_serde().expect_throw("Invalid response type");
|
||||||
|
Ok(Msg::PasswordResponseSuccess { status })
|
||||||
|
} else if status == 400 {
|
||||||
|
let kopid = headers.get("x-kanidm-opid").ok().flatten();
|
||||||
|
let jsval = JsFuture::from(resp.json()?).await?;
|
||||||
|
let status: OperationError = jsval.into_serde().expect_throw("Invalid response type");
|
||||||
|
match status {
|
||||||
|
OperationError::PasswordQuality(feedback) => {
|
||||||
|
Ok(Msg::PasswordResponseQuality { feedback })
|
||||||
|
}
|
||||||
|
e => Ok(Msg::Error {
|
||||||
|
emsg: format!("Invalid PWResp State Transition due to {:?}", e),
|
||||||
|
kopid,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let kopid = headers.get("x-kanidm-opid").ok().flatten();
|
||||||
|
let text = JsFuture::from(resp.text()?).await?;
|
||||||
|
let emsg = text.as_string().unwrap_or_else(|| "".to_string());
|
||||||
|
Ok(Msg::Error { emsg, kopid })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for PwModalApp {
|
||||||
|
type Message = Msg;
|
||||||
|
type Properties = ModalProps;
|
||||||
|
|
||||||
|
fn create(ctx: &Context<Self>) -> Self {
|
||||||
|
console::log!("pw modal create");
|
||||||
|
|
||||||
|
PwModalApp {
|
||||||
|
state: PwState::Init,
|
||||||
|
pw_check: PwCheck::Init,
|
||||||
|
pw_val: "".to_string(),
|
||||||
|
pw_check_val: "".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn changed(&mut self, _ctx: &Context<Self>) -> bool {
|
||||||
|
console::log!("pw modal::change");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||||
|
console::log!("pw modal::update");
|
||||||
|
match msg {
|
||||||
|
Msg::PasswordCheck => {
|
||||||
|
let pw =
|
||||||
|
utils::get_value_from_element_id("password").unwrap_or_else(|| "".to_string());
|
||||||
|
let check = utils::get_value_from_element_id("password-check")
|
||||||
|
.unwrap_or_else(|| "".to_string());
|
||||||
|
|
||||||
|
if pw == check {
|
||||||
|
self.pw_check = PwCheck::Valid
|
||||||
|
} else {
|
||||||
|
self.pw_check = PwCheck::Invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pw_val = pw;
|
||||||
|
self.pw_check_val = check;
|
||||||
|
}
|
||||||
|
Msg::PasswordCancel => {
|
||||||
|
self.reset_and_hide();
|
||||||
|
}
|
||||||
|
Msg::PasswordSubmit => {
|
||||||
|
self.state = PwState::Waiting;
|
||||||
|
|
||||||
|
let pw =
|
||||||
|
utils::get_value_from_element_id("password").unwrap_or_else(|| "".to_string());
|
||||||
|
let token_c = ctx.props().token.clone();
|
||||||
|
|
||||||
|
ctx.link().send_future(async {
|
||||||
|
match Self::submit_password_update(token_c, pw).await {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(v) => v.into(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Msg::PasswordResponseQuality { feedback } => self.state = PwState::Feedback(feedback),
|
||||||
|
Msg::PasswordResponseSuccess { status } => {
|
||||||
|
// Submit the update to the parent
|
||||||
|
EventBus::dispatcher().send(EventBusMsg::UpdateStatus { status });
|
||||||
|
self.reset_and_hide();
|
||||||
|
}
|
||||||
|
Msg::Error { emsg, kopid } => {
|
||||||
|
// Submit the error to the parent.
|
||||||
|
EventBus::dispatcher().send(EventBusMsg::Error { emsg, kopid });
|
||||||
|
self.reset_and_hide();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
|
||||||
|
console::log!("pw modal::rendered");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destroy(&mut self, _ctx: &Context<Self>) {
|
||||||
|
console::log!("pw modal::destroy");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
|
console::log!("pw modal::view");
|
||||||
|
|
||||||
|
let (pw_class, pw_feedback) = match &self.state {
|
||||||
|
PwState::Feedback(feedback) => {
|
||||||
|
let fb = html! {
|
||||||
|
<div id="password-validation-feedback" class="invalid-feedback">
|
||||||
|
<ul>
|
||||||
|
{
|
||||||
|
feedback.iter()
|
||||||
|
.map(|item| {
|
||||||
|
html! { <li>{ format!("{:?}", item) }</li> }
|
||||||
|
})
|
||||||
|
.collect::<Html>()
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
};
|
||||||
|
|
||||||
|
(classes!("form-control", "is-invalid"), fb)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let fb = html! {
|
||||||
|
<div id="password-validation-feedback" class="invalid-feedback">
|
||||||
|
</div>
|
||||||
|
};
|
||||||
|
(classes!("form-control"), fb)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let pw_check_class = match &self.pw_check {
|
||||||
|
PwCheck::Init => classes!("form-control"),
|
||||||
|
PwCheck::Valid => classes!("form-control", "is-valid"),
|
||||||
|
PwCheck::Invalid => classes!("form-control", "is-invalid"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let submit_enabled = match (&self.state, &self.pw_check) {
|
||||||
|
(PwState::Feedback(_), PwCheck::Valid) | (PwState::Init, PwCheck::Valid) => true,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let pw_val = self.pw_val.clone();
|
||||||
|
let pw_check_val = self.pw_check_val.clone();
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div class="modal fade" id="staticPassword" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="staticPasswordLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="staticPasswordLabel">{ "Add a New Password" }</h5>
|
||||||
|
<button type="button" class="btn-close" aria-label="Close"
|
||||||
|
onclick={
|
||||||
|
ctx.link()
|
||||||
|
.callback(move |_| {
|
||||||
|
Msg::PasswordCancel
|
||||||
|
})
|
||||||
|
}
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form class="row g-3 needs-validation" novalidate=true
|
||||||
|
onsubmit={ ctx.link().callback(move |e: FocusEvent| {
|
||||||
|
console::log!("pw modal::on form submit prevent default");
|
||||||
|
e.prevent_default();
|
||||||
|
if submit_enabled {
|
||||||
|
Msg::PasswordSubmit
|
||||||
|
} else {
|
||||||
|
Msg::PasswordCancel
|
||||||
|
}
|
||||||
|
} ) }
|
||||||
|
>
|
||||||
|
<label for="password" class="form-label">{ "Enter New Password" }</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class={ pw_class }
|
||||||
|
id="password"
|
||||||
|
placeholder=""
|
||||||
|
aria-describedby="password-validation-feedback"
|
||||||
|
value={ pw_val }
|
||||||
|
required=true
|
||||||
|
oninput={
|
||||||
|
ctx.link()
|
||||||
|
.callback(move |_| {
|
||||||
|
Msg::PasswordCheck
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{ pw_feedback }
|
||||||
|
<label for="password-check" class="form-label">{ "Repeat Password" }</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class={ pw_check_class }
|
||||||
|
id="password-check"
|
||||||
|
placeholder=""
|
||||||
|
aria-describedby="password-check-feedback"
|
||||||
|
value={ pw_check_val }
|
||||||
|
required=true
|
||||||
|
oninput={
|
||||||
|
ctx.link()
|
||||||
|
.callback(move |_| {
|
||||||
|
Msg::PasswordCheck
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button id="password-cancel" type="button" class="btn btn-secondary"
|
||||||
|
onclick={
|
||||||
|
ctx.link()
|
||||||
|
.callback(move |_| {
|
||||||
|
Msg::PasswordCancel
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>{ "Cancel" }</button>
|
||||||
|
<button id="password-submit" type="button" class="btn btn-primary"
|
||||||
|
disabled={ !submit_enabled }
|
||||||
|
onclick={
|
||||||
|
ctx.link()
|
||||||
|
.callback(move |_| {
|
||||||
|
Msg::PasswordSubmit
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>{ "Submit" }</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
530
kanidmd_web_ui/src/credential/reset.rs
Normal file
530
kanidmd_web_ui/src/credential/reset.rs
Normal file
|
@ -0,0 +1,530 @@
|
||||||
|
use crate::error::*;
|
||||||
|
use crate::models;
|
||||||
|
use crate::utils;
|
||||||
|
|
||||||
|
use gloo::console;
|
||||||
|
use yew::prelude::*;
|
||||||
|
use yew_agent::{Bridge, Bridged};
|
||||||
|
use yew_router::prelude::*;
|
||||||
|
|
||||||
|
use kanidm_proto::v1::{
|
||||||
|
CUIntentToken, CUSessionToken, CUStatus, CredentialDetail, CredentialDetailType,
|
||||||
|
};
|
||||||
|
|
||||||
|
use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt};
|
||||||
|
use wasm_bindgen_futures::JsFuture;
|
||||||
|
use web_sys::{Request, RequestInit, RequestMode, Response};
|
||||||
|
|
||||||
|
use super::delete::DeleteApp;
|
||||||
|
use super::eventbus::{EventBus, EventBusMsg};
|
||||||
|
use super::pwmodal::PwModalApp;
|
||||||
|
use super::totpmodal::TotpModalApp;
|
||||||
|
|
||||||
|
#[derive(PartialEq, Properties)]
|
||||||
|
pub struct ModalProps {
|
||||||
|
pub token: CUSessionToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Msg {
|
||||||
|
TokenSubmit,
|
||||||
|
BeginSession {
|
||||||
|
token: CUSessionToken,
|
||||||
|
status: CUStatus,
|
||||||
|
},
|
||||||
|
UpdateSession {
|
||||||
|
status: CUStatus,
|
||||||
|
},
|
||||||
|
Commit,
|
||||||
|
Success,
|
||||||
|
Error {
|
||||||
|
emsg: String,
|
||||||
|
kopid: Option<String>,
|
||||||
|
},
|
||||||
|
Ignore,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FetchError> for Msg {
|
||||||
|
fn from(fe: FetchError) -> Self {
|
||||||
|
Msg::Error {
|
||||||
|
emsg: fe.as_string(),
|
||||||
|
kopid: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum State {
|
||||||
|
TokenInput,
|
||||||
|
WaitingForStatus,
|
||||||
|
Main {
|
||||||
|
token: CUSessionToken,
|
||||||
|
status: CUStatus,
|
||||||
|
},
|
||||||
|
WaitingForCommit,
|
||||||
|
Error {
|
||||||
|
emsg: String,
|
||||||
|
kopid: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CredentialResetApp {
|
||||||
|
state: State,
|
||||||
|
eventbus: Box<dyn Bridge<EventBus>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for CredentialResetApp {
|
||||||
|
type Message = Msg;
|
||||||
|
type Properties = ();
|
||||||
|
|
||||||
|
fn create(ctx: &Context<Self>) -> Self {
|
||||||
|
console::log!("credential::reset::create");
|
||||||
|
|
||||||
|
// On a page refresh/reload, should we restart a session that *may* have existed?
|
||||||
|
// This could be achieved with local storage
|
||||||
|
|
||||||
|
// Where did we come from?
|
||||||
|
|
||||||
|
// Inject our class to centre everything.
|
||||||
|
if let Err(e) = crate::utils::body().class_list().add_1("form-signin-body") {
|
||||||
|
console::log!(format!("class_list add error -> {:?}", e));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Can we pre-load in a session token? This occures when we are sent a
|
||||||
|
// credential reset from the views UI.
|
||||||
|
|
||||||
|
/* Were we given a token for the reset? */
|
||||||
|
|
||||||
|
let location = ctx
|
||||||
|
.link()
|
||||||
|
.location()
|
||||||
|
.expect_throw("Can't access current location");
|
||||||
|
|
||||||
|
let query: Option<CUIntentToken> = location
|
||||||
|
.query()
|
||||||
|
.map_err(|e| {
|
||||||
|
let e_msg = format!("query decode error -> {:?}", e);
|
||||||
|
console::log!(e_msg.as_str());
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
let m_session = models::pop_cred_update_session();
|
||||||
|
|
||||||
|
let state = match (query, m_session) {
|
||||||
|
(Some(cu_intent), None) => {
|
||||||
|
// Go straight to go! Collect 200!
|
||||||
|
ctx.link().send_future(async {
|
||||||
|
match Self::exchange_intent_token(cu_intent.token).await {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(v) => v.into(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
State::WaitingForStatus
|
||||||
|
}
|
||||||
|
(None, Some((token, status))) => State::Main { token, status },
|
||||||
|
(None, None) => State::TokenInput,
|
||||||
|
(Some(_), Some(_)) => State::Error {
|
||||||
|
emsg: "Invalid State - Reset link and memory session both are available!"
|
||||||
|
.to_string(),
|
||||||
|
kopid: None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let eventbus = EventBus::bridge(ctx.link().callback(|req| match req {
|
||||||
|
EventBusMsg::UpdateStatus { status } => Msg::UpdateSession { status },
|
||||||
|
EventBusMsg::Error { emsg, kopid } => Msg::Error { emsg, kopid },
|
||||||
|
}));
|
||||||
|
|
||||||
|
CredentialResetApp { state, eventbus }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn changed(&mut self, _ctx: &Context<Self>) -> bool {
|
||||||
|
console::log!("credential::reset::change");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||||
|
console::log!("credential::reset::update");
|
||||||
|
let next_state = match (msg, &self.state) {
|
||||||
|
(Msg::Ignore, _) => None,
|
||||||
|
(Msg::TokenSubmit, State::TokenInput) => {
|
||||||
|
let token = utils::get_value_from_element_id("autofocus").expect("No token");
|
||||||
|
|
||||||
|
ctx.link().send_future(async {
|
||||||
|
match Self::exchange_intent_token(token).await {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(v) => v.into(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(State::WaitingForStatus)
|
||||||
|
}
|
||||||
|
(Msg::BeginSession { token, status }, State::WaitingForStatus) => {
|
||||||
|
console::log!(format!("{:?}", status).as_str());
|
||||||
|
Some(State::Main { token, status })
|
||||||
|
}
|
||||||
|
(Msg::UpdateSession { status }, State::Main { token, status: _ }) => {
|
||||||
|
console::log!(format!("{:?}", status).as_str());
|
||||||
|
Some(State::Main {
|
||||||
|
token: token.clone(),
|
||||||
|
status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
(Msg::Commit, State::Main { token, status }) => {
|
||||||
|
console::log!(format!("{:?}", status).as_str());
|
||||||
|
let token_c = token.clone();
|
||||||
|
|
||||||
|
ctx.link().send_future(async {
|
||||||
|
match Self::commit_session(token_c).await {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(v) => v.into(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(State::WaitingForCommit)
|
||||||
|
}
|
||||||
|
(Msg::Success, State::WaitingForCommit) => {
|
||||||
|
let loc = models::pop_return_location();
|
||||||
|
console::log!(format!("credential was updated, try going to -> {:?}", loc));
|
||||||
|
loc.goto(&ctx.link().history().expect_throw("failed to read history"));
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
(Msg::Error { emsg, kopid }, _) => Some(State::Error { emsg, kopid }),
|
||||||
|
(_, _) => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(mut next_state) = next_state {
|
||||||
|
std::mem::swap(&mut self.state, &mut next_state);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
|
||||||
|
crate::utils::autofocus();
|
||||||
|
console::log!("credential::reset::rendered");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
|
console::log!("credential::reset::view");
|
||||||
|
match &self.state {
|
||||||
|
State::TokenInput => self.view_token_input(ctx),
|
||||||
|
State::WaitingForStatus | State::WaitingForCommit => self.view_waiting(ctx),
|
||||||
|
State::Main { token, status } => self.view_main(ctx, &token, &status),
|
||||||
|
State::Error { emsg, kopid } => self.view_error(ctx, &emsg, kopid.as_deref()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destroy(&mut self, _ctx: &Context<Self>) {
|
||||||
|
console::log!("credential::reset::destroy");
|
||||||
|
if let Err(e) = crate::utils::body()
|
||||||
|
.class_list()
|
||||||
|
.remove_1("form-signin-body")
|
||||||
|
{
|
||||||
|
console::log!(format!("class_list remove error -> {:?}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CredentialResetApp {
|
||||||
|
fn view_token_input(&self, ctx: &Context<Self>) -> Html {
|
||||||
|
html! {
|
||||||
|
<main class="form-signin">
|
||||||
|
<div class="container">
|
||||||
|
<p>
|
||||||
|
{"Enter your credential reset token"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<form
|
||||||
|
onsubmit={ ctx.link().callback(|e: FocusEvent| {
|
||||||
|
console::log!("credential::reset::view_token_input -> TokenInput - prevent_default()");
|
||||||
|
e.prevent_default();
|
||||||
|
|
||||||
|
Msg::TokenSubmit
|
||||||
|
} ) }
|
||||||
|
action="javascript:void(0);"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="autofocus"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<button type="submit" class="btn btn-dark">{" Submit "}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_waiting(&self, _ctx: &Context<Self>) -> Html {
|
||||||
|
html! {
|
||||||
|
<main class="text-center form-signin h-100">
|
||||||
|
<div class="vert-center">
|
||||||
|
<div class="spinner-border text-dark" role="status">
|
||||||
|
<span class="visually-hidden">{ "Loading..." }</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_main(&self, ctx: &Context<Self>, token: &CUSessionToken, status: &CUStatus) -> Html {
|
||||||
|
if let Err(e) = crate::utils::body()
|
||||||
|
.class_list()
|
||||||
|
.remove_1("form-signin-body")
|
||||||
|
{
|
||||||
|
console::log!(format!("class_list remove error -> {:?}", e));
|
||||||
|
}
|
||||||
|
|
||||||
|
let displayname = status.displayname.clone();
|
||||||
|
let spn = status.spn.clone();
|
||||||
|
|
||||||
|
let can_commit = status.can_commit;
|
||||||
|
|
||||||
|
// match on primary, get type_.
|
||||||
|
// FUTURE: Need to work out based on policy if this is shown!
|
||||||
|
|
||||||
|
let pw_html = match &status.primary {
|
||||||
|
Some(CredentialDetail {
|
||||||
|
uuid: _,
|
||||||
|
claims: _,
|
||||||
|
type_: CredentialDetailType::Password,
|
||||||
|
}) => {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<p>{ "Password Set" }</p>
|
||||||
|
<p>{ "Mfa Disabled" }</p>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#staticTotpCreate">
|
||||||
|
{ "Add TOTP" }
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#staticDeletePrimaryCred">
|
||||||
|
{ "Delete this Password" }
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(CredentialDetail {
|
||||||
|
uuid: _,
|
||||||
|
claims: _,
|
||||||
|
type_: CredentialDetailType::GeneratedPassword,
|
||||||
|
}) => {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<p>{ "Generated Password" }</p>
|
||||||
|
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#staticDeletePrimaryCred">
|
||||||
|
{ "Delete this Password" }
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(CredentialDetail {
|
||||||
|
uuid: _,
|
||||||
|
claims: _,
|
||||||
|
type_: CredentialDetailType::Webauthn(_),
|
||||||
|
}) => {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<p>{ "Webauthn Only - Will migrate to trusted devices in a future update" }</p>
|
||||||
|
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#staticDeletePrimaryCred">
|
||||||
|
{ "Delete this Password" }
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(CredentialDetail {
|
||||||
|
uuid: _,
|
||||||
|
claims: _,
|
||||||
|
type_:
|
||||||
|
CredentialDetailType::PasswordMfa(
|
||||||
|
totp_set,
|
||||||
|
security_key_labels,
|
||||||
|
backup_codes_remaining,
|
||||||
|
),
|
||||||
|
}) => {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<p>{ "Password Set" }</p>
|
||||||
|
<p>{ "Mfa Enabled" }</p>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#staticTotpCreate">
|
||||||
|
{ "Reset TOTP" }
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#staticDeletePrimaryCred">
|
||||||
|
{ "Delete this MFA Credential" }
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
html! {
|
||||||
|
<button type="button" class="btn btn-warning" data-bs-toggle="modal" data-bs-target="#staticPassword">
|
||||||
|
{ "Add Password" }
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div class="d-flex align-items-start form-cred-reset-body">
|
||||||
|
<main class="w-100">
|
||||||
|
<div class="py-5 text-center">
|
||||||
|
<h4>{ "Updating Credentials" }</h4>
|
||||||
|
<p>{ displayname }</p>
|
||||||
|
<p>{ spn }</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<form class="needs-validation" novalidate=true>
|
||||||
|
<hr class="my-4" />
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#staticTrustedDevice">
|
||||||
|
{ "Add New Trusted Device" }
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<hr class="my-4" />
|
||||||
|
|
||||||
|
{ pw_html }
|
||||||
|
|
||||||
|
<hr class="my-4" />
|
||||||
|
<button class="w-100 btn btn-success btn-lg" type="submit"
|
||||||
|
disabled={ !can_commit }
|
||||||
|
onclick={
|
||||||
|
ctx.link()
|
||||||
|
.callback(move |_| {
|
||||||
|
Msg::Commit
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>{ "Submit Changes" }</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<div class="modal fade" id="staticTrustedDevice" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="staticTrustedDeviceLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="staticTrustedDeviceLabel">{ "Add a Trusted Device" }</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>{ "Scan the following link to add a new device" }</p>
|
||||||
|
|
||||||
|
<div class="spinner-border text-success" role="status">
|
||||||
|
<span class="visually-hidden">{ "Loading..." }</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{ "Cancel" }</button>
|
||||||
|
<button type="button" class="btn btn-primary">{ "Submit" }</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PwModalApp token={ token.clone() } />
|
||||||
|
|
||||||
|
<TotpModalApp token={ token.clone() }/>
|
||||||
|
|
||||||
|
<DeleteApp token= { token.clone() }/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
// <DelPrimaryModalApp token={ token.clone() }/>
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_error(&self, _ctx: &Context<Self>, msg: &str, kopid: Option<&str>) -> Html {
|
||||||
|
html! {
|
||||||
|
<main class="form-signin">
|
||||||
|
<div class="container">
|
||||||
|
<h2>{ "An Error Occured 🥺" }</h2>
|
||||||
|
</div>
|
||||||
|
<p>{ msg.to_string() }</p>
|
||||||
|
<p>
|
||||||
|
{
|
||||||
|
if let Some(opid) = kopid.as_ref() {
|
||||||
|
format!("Operation ID: {}", opid)
|
||||||
|
} else {
|
||||||
|
"Local Error".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</main>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn exchange_intent_token(token: String) -> Result<Msg, FetchError> {
|
||||||
|
let intentreq_jsvalue = serde_json::to_string(&CUIntentToken { token })
|
||||||
|
.map(|s| JsValue::from(&s))
|
||||||
|
.expect_throw("Failed to serialise intent request");
|
||||||
|
|
||||||
|
let mut opts = RequestInit::new();
|
||||||
|
opts.method("POST");
|
||||||
|
opts.mode(RequestMode::SameOrigin);
|
||||||
|
|
||||||
|
opts.body(Some(&intentreq_jsvalue));
|
||||||
|
|
||||||
|
let request = Request::new_with_str_and_init("/v1/credential/_exchange_intent", &opts)?;
|
||||||
|
request
|
||||||
|
.headers()
|
||||||
|
.set("content-type", "application/json")
|
||||||
|
.expect_throw("failed to set header");
|
||||||
|
|
||||||
|
let window = utils::window();
|
||||||
|
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
|
||||||
|
let resp: Response = resp_value.dyn_into().expect_throw("Invalid response type");
|
||||||
|
let status = resp.status();
|
||||||
|
let headers = resp.headers();
|
||||||
|
|
||||||
|
if status == 200 {
|
||||||
|
let jsval = JsFuture::from(resp.json()?).await?;
|
||||||
|
let (token, status): (CUSessionToken, CUStatus) =
|
||||||
|
jsval.into_serde().expect_throw("Invalid response type");
|
||||||
|
Ok(Msg::BeginSession { token, status })
|
||||||
|
} else {
|
||||||
|
let kopid = headers.get("x-kanidm-opid").ok().flatten();
|
||||||
|
let text = JsFuture::from(resp.text()?).await?;
|
||||||
|
let emsg = text.as_string().unwrap_or_else(|| "".to_string());
|
||||||
|
Ok(Msg::Error { emsg, kopid })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn commit_session(token: CUSessionToken) -> Result<Msg, FetchError> {
|
||||||
|
let req_jsvalue = serde_json::to_string(&token)
|
||||||
|
.map(|s| JsValue::from(&s))
|
||||||
|
.expect_throw("Failed to serialise session token");
|
||||||
|
|
||||||
|
let mut opts = RequestInit::new();
|
||||||
|
opts.method("POST");
|
||||||
|
opts.mode(RequestMode::SameOrigin);
|
||||||
|
|
||||||
|
opts.body(Some(&req_jsvalue));
|
||||||
|
|
||||||
|
let request = Request::new_with_str_and_init("/v1/credential/_commit", &opts)?;
|
||||||
|
request
|
||||||
|
.headers()
|
||||||
|
.set("content-type", "application/json")
|
||||||
|
.expect_throw("failed to set header");
|
||||||
|
|
||||||
|
let window = utils::window();
|
||||||
|
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
|
||||||
|
let resp: Response = resp_value.dyn_into().expect_throw("Invalid response type");
|
||||||
|
let status = resp.status();
|
||||||
|
let headers = resp.headers();
|
||||||
|
|
||||||
|
if status == 200 {
|
||||||
|
Ok(Msg::Success)
|
||||||
|
} else {
|
||||||
|
let kopid = headers.get("x-kanidm-opid").ok().flatten();
|
||||||
|
let text = JsFuture::from(resp.text()?).await?;
|
||||||
|
let emsg = text.as_string().unwrap_or_else(|| "".to_string());
|
||||||
|
Ok(Msg::Error { emsg, kopid })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
393
kanidmd_web_ui/src/credential/totpmodal.rs
Normal file
393
kanidmd_web_ui/src/credential/totpmodal.rs
Normal file
|
@ -0,0 +1,393 @@
|
||||||
|
use crate::error::*;
|
||||||
|
use crate::utils;
|
||||||
|
|
||||||
|
use super::eventbus::{EventBus, EventBusMsg};
|
||||||
|
use super::reset::ModalProps;
|
||||||
|
|
||||||
|
use gloo::console;
|
||||||
|
use web_sys::Node;
|
||||||
|
use yew::prelude::*;
|
||||||
|
use yew_agent::{Dispatched, Dispatcher};
|
||||||
|
|
||||||
|
use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt};
|
||||||
|
use wasm_bindgen_futures::JsFuture;
|
||||||
|
use web_sys::{Request, RequestInit, RequestMode, Response};
|
||||||
|
|
||||||
|
use kanidm_proto::v1::{
|
||||||
|
CURegState, CURequest, CUSessionToken, CUStatus, OperationError, PasswordFeedback, TotpSecret,
|
||||||
|
};
|
||||||
|
use qrcode::{render::svg, QrCode};
|
||||||
|
|
||||||
|
enum TotpState {
|
||||||
|
Init,
|
||||||
|
Waiting,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TotpCheck {
|
||||||
|
Init,
|
||||||
|
Invalid,
|
||||||
|
Sha1Accept,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TotpValue {
|
||||||
|
Init,
|
||||||
|
Secret(TotpSecret),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TotpModalApp {
|
||||||
|
state: TotpState,
|
||||||
|
check: TotpCheck,
|
||||||
|
secret: TotpValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Msg {
|
||||||
|
TotpCancel,
|
||||||
|
TotpSubmit,
|
||||||
|
TotpSecretReady(TotpSecret),
|
||||||
|
TotpTryAgain,
|
||||||
|
TotpInvalidSha1,
|
||||||
|
Error { emsg: String, kopid: Option<String> },
|
||||||
|
TotpAcceptSha1,
|
||||||
|
TotpSuccess,
|
||||||
|
TotpClearInvalid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FetchError> for Msg {
|
||||||
|
fn from(fe: FetchError) -> Self {
|
||||||
|
Msg::Error {
|
||||||
|
emsg: fe.as_string(),
|
||||||
|
kopid: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TotpModalApp {
|
||||||
|
fn reset_and_hide(&mut self) {
|
||||||
|
utils::modal_hide_by_id("staticTotpCreate");
|
||||||
|
self.state = TotpState::Init;
|
||||||
|
self.check = TotpCheck::Init;
|
||||||
|
self.secret = TotpValue::Init;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn submit_totp_update(token: CUSessionToken, req: CURequest) -> Result<Msg, FetchError> {
|
||||||
|
let req_jsvalue = serde_json::to_string(&(req, token))
|
||||||
|
.map(|s| JsValue::from(&s))
|
||||||
|
.expect_throw("Failed to serialise pw curequest");
|
||||||
|
|
||||||
|
let mut opts = RequestInit::new();
|
||||||
|
opts.method("POST");
|
||||||
|
opts.mode(RequestMode::SameOrigin);
|
||||||
|
|
||||||
|
opts.body(Some(&req_jsvalue));
|
||||||
|
|
||||||
|
let request = Request::new_with_str_and_init("/v1/credential/_update", &opts)?;
|
||||||
|
request
|
||||||
|
.headers()
|
||||||
|
.set("content-type", "application/json")
|
||||||
|
.expect_throw("failed to set header");
|
||||||
|
|
||||||
|
let window = utils::window();
|
||||||
|
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
|
||||||
|
let resp: Response = resp_value.dyn_into().expect_throw("Invalid response type");
|
||||||
|
let status = resp.status();
|
||||||
|
let headers = resp.headers();
|
||||||
|
|
||||||
|
let kopid = headers.get("x-kanidm-opid").ok().flatten();
|
||||||
|
|
||||||
|
if status == 200 {
|
||||||
|
let jsval = JsFuture::from(resp.json()?).await?;
|
||||||
|
let status: CUStatus = jsval.into_serde().expect_throw("Invalid response type");
|
||||||
|
|
||||||
|
EventBus::dispatcher().send(EventBusMsg::UpdateStatus {
|
||||||
|
status: status.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(match status.mfaregstate {
|
||||||
|
CURegState::BackupCodes(_) => Msg::Error {
|
||||||
|
emsg: "Invalid TOTP mfa reg state response".to_string(),
|
||||||
|
kopid,
|
||||||
|
},
|
||||||
|
CURegState::None => Msg::TotpSuccess,
|
||||||
|
CURegState::TotpCheck(secret) => Msg::TotpSecretReady(secret),
|
||||||
|
CURegState::TotpTryAgain => Msg::TotpTryAgain,
|
||||||
|
CURegState::TotpInvalidSha1 => Msg::TotpInvalidSha1,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let text = JsFuture::from(resp.text()?).await?;
|
||||||
|
let emsg = text.as_string().unwrap_or_else(|| "".to_string());
|
||||||
|
Ok(Msg::Error { emsg, kopid })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for TotpModalApp {
|
||||||
|
type Message = Msg;
|
||||||
|
type Properties = ModalProps;
|
||||||
|
|
||||||
|
fn create(ctx: &Context<Self>) -> Self {
|
||||||
|
console::log!("totp modal create");
|
||||||
|
|
||||||
|
// SEND OFF A REQUEST TO GET THE TOTP STRING
|
||||||
|
let token_c = ctx.props().token.clone();
|
||||||
|
|
||||||
|
ctx.link().send_future(async {
|
||||||
|
match Self::submit_totp_update(token_c, CURequest::TotpGenerate).await {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(v) => v.into(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
TotpModalApp {
|
||||||
|
state: TotpState::Init,
|
||||||
|
check: TotpCheck::Init,
|
||||||
|
secret: TotpValue::Init,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn changed(&mut self, _ctx: &Context<Self>) -> bool {
|
||||||
|
console::log!("totp modal::change");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||||
|
console::log!("totp modal::update");
|
||||||
|
let token_c = ctx.props().token.clone();
|
||||||
|
match msg {
|
||||||
|
Msg::TotpCancel => {
|
||||||
|
// Cancel the totp req!
|
||||||
|
// Should end up with a success?
|
||||||
|
ctx.link().send_future(async {
|
||||||
|
match Self::submit_totp_update(token_c, CURequest::CancelMFAReg).await {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(v) => v.into(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.state = TotpState::Waiting;
|
||||||
|
}
|
||||||
|
Msg::TotpSubmit => {
|
||||||
|
// Send off the submit, lock the form.
|
||||||
|
let totp =
|
||||||
|
utils::get_value_from_element_id("totp").unwrap_or_else(|| "".to_string());
|
||||||
|
|
||||||
|
match totp.trim().parse::<u32>() {
|
||||||
|
Ok(totp) => {
|
||||||
|
ctx.link().send_future(async move {
|
||||||
|
match Self::submit_totp_update(token_c, CURequest::TotpVerify(totp))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(v) => v.into(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.state = TotpState::Waiting;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
self.check = TotpCheck::Invalid;
|
||||||
|
self.state = TotpState::Init;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Msg::TotpSecretReady(secret) => {
|
||||||
|
// THIS IS WHATS CALLED WHEN THE SECRET IS BACK
|
||||||
|
self.secret = TotpValue::Secret(secret);
|
||||||
|
}
|
||||||
|
Msg::TotpTryAgain => {
|
||||||
|
self.check = TotpCheck::Invalid;
|
||||||
|
self.state = TotpState::Init;
|
||||||
|
}
|
||||||
|
Msg::TotpClearInvalid => {
|
||||||
|
self.check = TotpCheck::Init;
|
||||||
|
}
|
||||||
|
Msg::TotpInvalidSha1 => {
|
||||||
|
self.check = TotpCheck::Sha1Accept;
|
||||||
|
self.state = TotpState::Init;
|
||||||
|
}
|
||||||
|
Msg::TotpAcceptSha1 => {
|
||||||
|
ctx.link().send_future(async {
|
||||||
|
match Self::submit_totp_update(token_c, CURequest::TotpAcceptSha1).await {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(v) => v.into(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.state = TotpState::Waiting;
|
||||||
|
}
|
||||||
|
Msg::TotpClearInvalid => {
|
||||||
|
self.check = TotpCheck::Invalid;
|
||||||
|
}
|
||||||
|
Msg::TotpSuccess => {
|
||||||
|
// Nothing to do but close and hide!
|
||||||
|
self.reset_and_hide();
|
||||||
|
}
|
||||||
|
Msg::Error { emsg, kopid } => {
|
||||||
|
// Submit the error to the parent.
|
||||||
|
EventBus::dispatcher().send(EventBusMsg::Error { emsg, kopid });
|
||||||
|
self.reset_and_hide();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
|
||||||
|
console::log!("totp modal::rendered");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destroy(&mut self, _ctx: &Context<Self>) {
|
||||||
|
console::log!("totp modal::destroy");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
|
console::log!("totp modal::view");
|
||||||
|
|
||||||
|
let totp_class = match &self.check {
|
||||||
|
TotpCheck::Invalid | TotpCheck::Sha1Accept => classes!("form-control", "is-invalid"),
|
||||||
|
_ => classes!("form-control"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let invalid_text = match &self.check {
|
||||||
|
TotpCheck::Sha1Accept => "Your authenticator appears to be broken, and uses Sha1, rather than Sha256. Are you sure you want to proceed? If you want to try with a new authenticator, enter a new code",
|
||||||
|
_ => "Incorrect TOTP code - Please try again",
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
let submit_enabled = match &self.state {
|
||||||
|
TotpState::Init => true,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let submit_button = match &self.check {
|
||||||
|
TotpCheck::Sha1Accept => html! {
|
||||||
|
<button id="totp-submit" type="button" class="btn btn-warning"
|
||||||
|
disabled={ !submit_enabled }
|
||||||
|
onclick={
|
||||||
|
ctx.link()
|
||||||
|
.callback(move |_| {
|
||||||
|
Msg::TotpAcceptSha1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>{ "Accept Sha1 Token" }</button>
|
||||||
|
},
|
||||||
|
_ => html! {
|
||||||
|
<button id="totp-submit" type="button" class="btn btn-primary"
|
||||||
|
disabled={ !submit_enabled }
|
||||||
|
onclick={
|
||||||
|
ctx.link()
|
||||||
|
.callback(move |_| {
|
||||||
|
Msg::TotpSubmit
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>{ "Submit" }</button>
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let totp_secret_state = match &self.secret {
|
||||||
|
TotpValue::Init => {
|
||||||
|
html! {
|
||||||
|
<div class="spinner-border text-dark" role="status">
|
||||||
|
<span class="visually-hidden">{ "Loading..." }</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TotpValue::Secret(secret) => {
|
||||||
|
let qr = QrCode::new(secret.to_uri().as_str()).unwrap_throw();
|
||||||
|
|
||||||
|
let svg = qr.render::<svg::Color>().build();
|
||||||
|
|
||||||
|
let div = utils::document().create_element("div").unwrap();
|
||||||
|
|
||||||
|
div.set_inner_html(svg.as_str());
|
||||||
|
|
||||||
|
let node: Node = div.into();
|
||||||
|
let svg_html = Html::VRef(node);
|
||||||
|
|
||||||
|
let accountname = format!("Account Name: {}", secret.accountname);
|
||||||
|
let issuer = format!("Issuer: {}", secret.issuer);
|
||||||
|
let secret_b32 = format!("Secret: {}", secret.get_secret());
|
||||||
|
let algo = format!("Algorithm: {}", secret.algo);
|
||||||
|
let step = format!("Time Step: {}", secret.step);
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<div class="col-8">
|
||||||
|
{ svg_html }
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<p>{ accountname }</p>
|
||||||
|
<p>{ issuer }</p>
|
||||||
|
<p>{ secret_b32 }</p>
|
||||||
|
<p>{ algo }</p>
|
||||||
|
<p>{ step }</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div class="modal fade" id="staticTotpCreate" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="staticTotpCreate" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="staticTotpLabel">{ "Add a New TOTP Authenticator" }</h5>
|
||||||
|
<button type="button" class="btn-close" aria-label="Close"
|
||||||
|
onclick={
|
||||||
|
ctx.link()
|
||||||
|
.callback(move |_| {
|
||||||
|
Msg::TotpCancel
|
||||||
|
})
|
||||||
|
}
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
{ totp_secret_state }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="row g-3 needs-validation" novalidate=true
|
||||||
|
onsubmit={ ctx.link().callback(|e: FocusEvent| {
|
||||||
|
e.prevent_default();
|
||||||
|
Msg::TotpSubmit
|
||||||
|
} ) }
|
||||||
|
>
|
||||||
|
<label for="totp" class="form-label">{ "Enter a TOTP" }</label>
|
||||||
|
<input
|
||||||
|
type="totp"
|
||||||
|
class={ totp_class }
|
||||||
|
id="totp"
|
||||||
|
placeholder=""
|
||||||
|
aria-describedby="totp-validation-feedback"
|
||||||
|
required=true
|
||||||
|
oninput={
|
||||||
|
ctx.link()
|
||||||
|
.callback(move |_| {
|
||||||
|
Msg::TotpClearInvalid
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div id="totp-validation-feedback" class="invalid-feedback">
|
||||||
|
{ invalid_text }
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button id="totp-cancel" type="button" class="btn btn-secondary"
|
||||||
|
onclick={
|
||||||
|
ctx.link()
|
||||||
|
.callback(move |_| {
|
||||||
|
Msg::TotpCancel
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>{ "Cancel" }</button>
|
||||||
|
{ submit_button }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
kanidmd_web_ui/src/external/bootstrap.bundle.min.js.map
vendored
Normal file
1
kanidmd_web_ui/src/external/bootstrap.bundle.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
kanidmd_web_ui/src/external/bootstrap.min.css.map
vendored
Normal file
1
kanidmd_web_ui/src/external/bootstrap.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
3
kanidmd_web_ui/src/favicon.svg
Normal file
3
kanidmd_web_ui/src/favicon.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||||
|
<text y=".9em" font-size="90">🦀</text>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 111 B |
|
@ -1,5 +1,5 @@
|
||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "256"]
|
||||||
#![deny(warnings)]
|
// #![deny(warnings)]
|
||||||
#![warn(unused_extern_crates)]
|
#![warn(unused_extern_crates)]
|
||||||
#![deny(clippy::todo)]
|
#![deny(clippy::todo)]
|
||||||
#![deny(clippy::unimplemented)]
|
#![deny(clippy::unimplemented)]
|
||||||
|
@ -13,6 +13,7 @@
|
||||||
|
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
mod credential;
|
||||||
mod error;
|
mod error;
|
||||||
mod login;
|
mod login;
|
||||||
mod manager;
|
mod manager;
|
||||||
|
|
|
@ -10,6 +10,7 @@ use yew::functional::*;
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use yew_router::prelude::*;
|
use yew_router::prelude::*;
|
||||||
|
|
||||||
|
use crate::credential::reset::CredentialResetApp;
|
||||||
use crate::login::LoginApp;
|
use crate::login::LoginApp;
|
||||||
use crate::oauth2::Oauth2App;
|
use crate::oauth2::Oauth2App;
|
||||||
use crate::views::{ViewRoute, ViewsApp};
|
use crate::views::{ViewRoute, ViewsApp};
|
||||||
|
@ -30,8 +31,11 @@ pub enum Route {
|
||||||
#[at("/ui/oauth2")]
|
#[at("/ui/oauth2")]
|
||||||
Oauth2,
|
Oauth2,
|
||||||
|
|
||||||
|
#[at("/ui/reset")]
|
||||||
|
CredentialReset,
|
||||||
|
|
||||||
#[not_found]
|
#[not_found]
|
||||||
#[at("/404")]
|
#[at("/ui/404")]
|
||||||
NotFound,
|
NotFound,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,6 +55,7 @@ fn switch(route: &Route) -> Html {
|
||||||
Route::Login => html! { <LoginApp /> },
|
Route::Login => html! { <LoginApp /> },
|
||||||
Route::Oauth2 => html! { <Oauth2App /> },
|
Route::Oauth2 => html! { <Oauth2App /> },
|
||||||
Route::Views => html! { <ViewsApp /> },
|
Route::Views => html! { <ViewsApp /> },
|
||||||
|
Route::CredentialReset => html! { <CredentialResetApp /> },
|
||||||
Route::NotFound => {
|
Route::NotFound => {
|
||||||
html! {
|
html! {
|
||||||
<main>
|
<main>
|
||||||
|
|
|
@ -11,6 +11,8 @@ use crate::manager::Route;
|
||||||
use crate::views::ViewRoute;
|
use crate::views::ViewRoute;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use kanidm_proto::v1::{CUSessionToken, CUStatus};
|
||||||
|
|
||||||
pub fn get_bearer_token() -> Option<String> {
|
pub fn get_bearer_token() -> Option<String> {
|
||||||
let prev_session: Result<String, _> = PersistentStorage::get("kanidm_bearer_token");
|
let prev_session: Result<String, _> = PersistentStorage::get("kanidm_bearer_token");
|
||||||
console::log!(format!("kanidm_bearer_token -> {:?}", prev_session).as_str());
|
console::log!(format!("kanidm_bearer_token -> {:?}", prev_session).as_str());
|
||||||
|
@ -74,3 +76,14 @@ pub fn pop_login_hint() -> Option<String> {
|
||||||
TemporaryStorage::delete("login_hint");
|
TemporaryStorage::delete("login_hint");
|
||||||
l.ok()
|
l.ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn push_cred_update_session(s: (CUSessionToken, CUStatus)) {
|
||||||
|
TemporaryStorage::set("cred_update_session", s)
|
||||||
|
.expect_throw("failed to set cred session token");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pop_cred_update_session() -> Option<(CUSessionToken, CUStatus)> {
|
||||||
|
let l: Result<(CUSessionToken, CUStatus), _> = TemporaryStorage::get("cred_update_session");
|
||||||
|
TemporaryStorage::delete("cred_update_session");
|
||||||
|
l.ok()
|
||||||
|
}
|
||||||
|
|
|
@ -359,7 +359,7 @@ impl Component for Oauth2App {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
console::log!("login::view");
|
console::log!("oauth2::view");
|
||||||
match &self.state {
|
match &self.state {
|
||||||
State::LoginRequired => {
|
State::LoginRequired => {
|
||||||
// <body class="html-body form-body">
|
// <body class="html-body form-body">
|
||||||
|
|
|
@ -3,6 +3,13 @@ body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-cred-reset-body {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
padding: 15px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.form-signin-body {
|
.form-signin-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -140,3 +147,10 @@ body {
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
|
box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vert-center {
|
||||||
|
height: 80%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
use gloo::console;
|
use gloo::console;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
use wasm_bindgen::{JsCast, UnwrapThrowExt};
|
use wasm_bindgen::{JsCast, UnwrapThrowExt};
|
||||||
pub use web_sys::InputEvent;
|
pub use web_sys::InputEvent;
|
||||||
use web_sys::{Document, Event, HtmlElement, HtmlInputElement, Window};
|
use web_sys::{Document, Event, HtmlButtonElement, HtmlElement, HtmlInputElement, Window};
|
||||||
|
|
||||||
pub fn window() -> Window {
|
pub fn window() -> Window {
|
||||||
web_sys::window().expect_throw("Unable to retrieve window")
|
web_sys::window().expect_throw("Unable to retrieve window")
|
||||||
|
@ -35,3 +36,41 @@ pub fn get_value_from_input_event(e: InputEvent) -> String {
|
||||||
let target: HtmlInputElement = event_target.dyn_into().unwrap_throw();
|
let target: HtmlInputElement = event_target.dyn_into().unwrap_throw();
|
||||||
target.value()
|
target.value()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_element_by_id(id: &str) -> Option<HtmlElement> {
|
||||||
|
document()
|
||||||
|
.get_element_by_id(id)
|
||||||
|
.and_then(|element| element.dyn_into::<web_sys::HtmlElement>().ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_buttonelement_by_id(id: &str) -> Option<HtmlButtonElement> {
|
||||||
|
document()
|
||||||
|
.get_element_by_id(id)
|
||||||
|
.and_then(|element| element.dyn_into::<web_sys::HtmlButtonElement>().ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_inputelement_by_id(id: &str) -> Option<HtmlInputElement> {
|
||||||
|
document()
|
||||||
|
.get_element_by_id(id)
|
||||||
|
.and_then(|element| element.dyn_into::<web_sys::HtmlInputElement>().ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_value_from_element_id(id: &str) -> Option<String> {
|
||||||
|
document()
|
||||||
|
.get_element_by_id(id)
|
||||||
|
.and_then(|element| element.dyn_into::<web_sys::HtmlInputElement>().ok())
|
||||||
|
.map(|element| element.value())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(inline_js = "export function modal_hide(m) {
|
||||||
|
var elem = document.getElementById(m);
|
||||||
|
var modal = bootstrap.Modal.getInstance(elem);
|
||||||
|
modal.hide();
|
||||||
|
}")]
|
||||||
|
extern "C" {
|
||||||
|
fn modal_hide(m: &str);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn modal_hide_by_id(id: &str) {
|
||||||
|
modal_hide(id);
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
use crate::error::*;
|
||||||
use crate::models;
|
use crate::models;
|
||||||
|
use crate::utils;
|
||||||
use gloo::console;
|
use gloo::console;
|
||||||
use wasm_bindgen::UnwrapThrowExt;
|
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
use crate::manager::Route;
|
use crate::manager::Route;
|
||||||
|
@ -8,6 +9,10 @@ use yew_router::prelude::*;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt};
|
||||||
|
use wasm_bindgen_futures::JsFuture;
|
||||||
|
use web_sys::{Request, RequestInit, RequestMode, Response};
|
||||||
|
|
||||||
mod apps;
|
mod apps;
|
||||||
mod components;
|
mod components;
|
||||||
mod security;
|
mod security;
|
||||||
|
@ -30,7 +35,14 @@ pub enum ViewRoute {
|
||||||
|
|
||||||
enum State {
|
enum State {
|
||||||
LoginRequired,
|
LoginRequired,
|
||||||
|
Verifying,
|
||||||
Authenticated(String),
|
Authenticated(String),
|
||||||
|
Error { emsg: String, kopid: Option<String> },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Properties)]
|
||||||
|
pub struct ViewProps {
|
||||||
|
pub token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ViewsApp {
|
pub struct ViewsApp {
|
||||||
|
@ -38,14 +50,30 @@ pub struct ViewsApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum ViewsMsg {
|
pub enum ViewsMsg {
|
||||||
|
Verified(String),
|
||||||
Logout,
|
Logout,
|
||||||
|
Error { emsg: String, kopid: Option<String> },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FetchError> for ViewsMsg {
|
||||||
|
fn from(fe: FetchError) -> Self {
|
||||||
|
ViewsMsg::Error {
|
||||||
|
emsg: fe.as_string(),
|
||||||
|
kopid: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn switch(route: &ViewRoute) -> Html {
|
fn switch(route: &ViewRoute) -> Html {
|
||||||
console::log!("views::switch");
|
console::log!("views::switch");
|
||||||
|
|
||||||
|
// safety - can't panic because to get to this location we MUST be authenticated!
|
||||||
|
let token =
|
||||||
|
models::get_bearer_token().expect_throw("Invalid state, bearer token must be present!");
|
||||||
|
|
||||||
match route {
|
match route {
|
||||||
ViewRoute::Apps => html! { <AppsApp /> },
|
ViewRoute::Apps => html! { <AppsApp /> },
|
||||||
ViewRoute::Security => html! { <SecurityApp /> },
|
ViewRoute::Security => html! { <SecurityApp token={ token } /> },
|
||||||
ViewRoute::NotFound => html! {
|
ViewRoute::NotFound => html! {
|
||||||
<Redirect<Route> to={Route::NotFound}/>
|
<Redirect<Route> to={Route::NotFound}/>
|
||||||
},
|
},
|
||||||
|
@ -56,12 +84,26 @@ impl Component for ViewsApp {
|
||||||
type Message = ViewsMsg;
|
type Message = ViewsMsg;
|
||||||
type Properties = ();
|
type Properties = ();
|
||||||
|
|
||||||
fn create(_ctx: &Context<Self>) -> Self {
|
fn create(ctx: &Context<Self>) -> Self {
|
||||||
console::log!("views::create");
|
console::log!("views::create");
|
||||||
|
|
||||||
let state = models::get_bearer_token()
|
// Ensure the token is valid before we proceed. Could be
|
||||||
.map(State::Authenticated)
|
// due to a session expiry or something else, but we want to make
|
||||||
.unwrap_or(State::LoginRequired);
|
// sure we are really authenticated before we proceed.
|
||||||
|
let state = match models::get_bearer_token() {
|
||||||
|
Some(token) => {
|
||||||
|
// Send off the validation event.
|
||||||
|
ctx.link().send_future(async {
|
||||||
|
match Self::check_token_valid(token).await {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(v) => v.into(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
State::Verifying
|
||||||
|
}
|
||||||
|
None => State::LoginRequired,
|
||||||
|
};
|
||||||
|
|
||||||
ViewsApp { state }
|
ViewsApp { state }
|
||||||
}
|
}
|
||||||
|
@ -74,11 +116,19 @@ impl Component for ViewsApp {
|
||||||
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||||
console::log!("views::update");
|
console::log!("views::update");
|
||||||
match msg {
|
match msg {
|
||||||
|
ViewsMsg::Verified(token) => {
|
||||||
|
self.state = State::Authenticated(token);
|
||||||
|
true
|
||||||
|
}
|
||||||
ViewsMsg::Logout => {
|
ViewsMsg::Logout => {
|
||||||
models::clear_bearer_token();
|
models::clear_bearer_token();
|
||||||
self.state = State::LoginRequired;
|
self.state = State::LoginRequired;
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
ViewsMsg::Error { emsg, kopid } => {
|
||||||
|
self.state = State::Error { emsg, kopid };
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,7 +137,7 @@ impl Component for ViewsApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
match self.state {
|
match &self.state {
|
||||||
State::LoginRequired => {
|
State::LoginRequired => {
|
||||||
// Where are we?
|
// Where are we?
|
||||||
let loc = ctx
|
let loc = ctx
|
||||||
|
@ -106,7 +156,37 @@ impl Component for ViewsApp {
|
||||||
.push(Route::Login);
|
.push(Route::Login);
|
||||||
html! { <div></div> }
|
html! { <div></div> }
|
||||||
}
|
}
|
||||||
|
State::Verifying => {
|
||||||
|
html! {
|
||||||
|
<main class="text-center form-signin h-100">
|
||||||
|
<div class="vert-center">
|
||||||
|
<div class="spinner-border text-dark" role="status">
|
||||||
|
<span class="visually-hidden">{ "Loading..." }</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
}
|
||||||
|
}
|
||||||
State::Authenticated(_) => self.view_authenticated(ctx),
|
State::Authenticated(_) => self.view_authenticated(ctx),
|
||||||
|
State::Error { emsg, kopid } => {
|
||||||
|
html! {
|
||||||
|
<main class="form-signin">
|
||||||
|
<div class="container">
|
||||||
|
<h2>{ "An Error Occured 🥺" }</h2>
|
||||||
|
</div>
|
||||||
|
<p>{ emsg.to_string() }</p>
|
||||||
|
<p>
|
||||||
|
{
|
||||||
|
if let Some(opid) = kopid.as_ref() {
|
||||||
|
format!("Operation ID: {}", opid)
|
||||||
|
} else {
|
||||||
|
"Local Error".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</main>
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -160,4 +240,39 @@ impl ViewsApp {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn check_token_valid(token: String) -> Result<ViewsMsg, FetchError> {
|
||||||
|
let mut opts = RequestInit::new();
|
||||||
|
opts.method("GET");
|
||||||
|
opts.mode(RequestMode::SameOrigin);
|
||||||
|
|
||||||
|
let request = Request::new_with_str_and_init("/v1/auth/valid", &opts)?;
|
||||||
|
|
||||||
|
request
|
||||||
|
.headers()
|
||||||
|
.set("content-type", "application/json")
|
||||||
|
.expect_throw("failed to set header");
|
||||||
|
request
|
||||||
|
.headers()
|
||||||
|
.set("authorization", format!("Bearer {}", token).as_str())
|
||||||
|
.expect_throw("failed to set header");
|
||||||
|
|
||||||
|
let window = utils::window();
|
||||||
|
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
|
||||||
|
let resp: Response = resp_value.dyn_into().expect_throw("Invalid response type");
|
||||||
|
let status = resp.status();
|
||||||
|
|
||||||
|
if status == 200 {
|
||||||
|
Ok(ViewsMsg::Verified(token))
|
||||||
|
} else if status == 401 {
|
||||||
|
// Not valid, re-auth
|
||||||
|
Ok(ViewsMsg::Logout)
|
||||||
|
} else {
|
||||||
|
let headers = resp.headers();
|
||||||
|
let kopid = headers.get("x-kanidm-opid").ok().flatten();
|
||||||
|
let text = JsFuture::from(resp.text()?).await?;
|
||||||
|
let emsg = text.as_string().unwrap_or_else(|| "".to_string());
|
||||||
|
Ok(ViewsMsg::Error { emsg, kopid })
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,61 @@
|
||||||
|
use crate::error::*;
|
||||||
|
use crate::models;
|
||||||
|
use crate::utils;
|
||||||
|
|
||||||
|
use crate::manager::Route;
|
||||||
|
use crate::views::{ViewProps, ViewRoute};
|
||||||
|
|
||||||
|
use compact_jwt::{Jws, JwsUnverified};
|
||||||
use gloo::console;
|
use gloo::console;
|
||||||
|
use std::str::FromStr;
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
use yew_router::prelude::*;
|
||||||
|
|
||||||
|
use kanidm_proto::v1::{CUSessionToken, CUStatus, UserAuthToken};
|
||||||
|
|
||||||
|
use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt};
|
||||||
|
use wasm_bindgen_futures::JsFuture;
|
||||||
|
use web_sys::{Request, RequestInit, RequestMode, Response};
|
||||||
|
|
||||||
pub enum Msg {
|
pub enum Msg {
|
||||||
// Nothing
|
// Nothing
|
||||||
|
RequestCredentialUpdate,
|
||||||
|
BeginCredentialUpdate {
|
||||||
|
token: CUSessionToken,
|
||||||
|
status: CUStatus,
|
||||||
|
},
|
||||||
|
Error {
|
||||||
|
emsg: String,
|
||||||
|
kopid: Option<String>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SecurityApp {}
|
impl From<FetchError> for Msg {
|
||||||
|
fn from(fe: FetchError) -> Self {
|
||||||
|
Msg::Error {
|
||||||
|
emsg: fe.as_string(),
|
||||||
|
kopid: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum State {
|
||||||
|
Init,
|
||||||
|
Waiting,
|
||||||
|
Error { emsg: String, kopid: Option<String> },
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SecurityApp {
|
||||||
|
state: State,
|
||||||
|
}
|
||||||
|
|
||||||
impl Component for SecurityApp {
|
impl Component for SecurityApp {
|
||||||
type Message = Msg;
|
type Message = Msg;
|
||||||
type Properties = ();
|
type Properties = ViewProps;
|
||||||
|
|
||||||
fn create(_ctx: &Context<Self>) -> Self {
|
fn create(_ctx: &Context<Self>) -> Self {
|
||||||
console::log!("views::security::create");
|
console::log!("views::security::create");
|
||||||
SecurityApp {}
|
SecurityApp { state: State::Init }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn changed(&mut self, _ctx: &Context<Self>) -> bool {
|
fn changed(&mut self, _ctx: &Context<Self>) -> bool {
|
||||||
|
@ -21,26 +63,140 @@ impl Component for SecurityApp {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, _ctx: &Context<Self>, _msg: Self::Message) -> bool {
|
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||||
console::log!("views::security::update");
|
console::log!("views::security::update");
|
||||||
/*
|
|
||||||
match msg {
|
match msg {
|
||||||
ViewsMsg::Logout => {
|
Msg::RequestCredentialUpdate => {
|
||||||
|
// Submit a req to init the session.
|
||||||
|
// The uuid we want to submit against - hint, it's us.
|
||||||
|
let token = ctx.props().token.clone();
|
||||||
|
|
||||||
|
let jwtu =
|
||||||
|
JwsUnverified::from_str(&token).expect_throw("Invalid UAT, unable to parse");
|
||||||
|
|
||||||
|
let uat: Jws<UserAuthToken> = jwtu
|
||||||
|
.unsafe_release_without_verification()
|
||||||
|
.expect_throw("Unvalid UAT, unable to release ");
|
||||||
|
|
||||||
|
let id = uat.inner.uuid.to_string();
|
||||||
|
|
||||||
|
ctx.link().send_future(async {
|
||||||
|
match Self::fetch_token_valid(id, token).await {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(v) => v.into(),
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
*/
|
|
||||||
|
self.state = State::Waiting;
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
Msg::BeginCredentialUpdate { token, status } => {
|
||||||
|
// Got the rec, setup.
|
||||||
|
models::push_cred_update_session((token, status));
|
||||||
|
models::push_return_location(models::Location::Views(ViewRoute::Security));
|
||||||
|
|
||||||
|
ctx.link()
|
||||||
|
.history()
|
||||||
|
.expect_throw("failed to read history")
|
||||||
|
.push(Route::CredentialReset);
|
||||||
|
// No need to redraw, or reset state, since this redirect will destroy
|
||||||
|
// the state.
|
||||||
|
false
|
||||||
|
}
|
||||||
|
Msg::Error { emsg, kopid } => {
|
||||||
|
self.state = State::Error { emsg, kopid };
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
|
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
|
||||||
console::log!("views::security::rendered");
|
console::log!("views::security::rendered");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(&self, _ctx: &Context<Self>) -> Html {
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
|
let submit_enabled = match self.state {
|
||||||
|
State::Init | State::Error { .. } => true,
|
||||||
|
State::Waiting => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let error = match &self.state {
|
||||||
|
State::Error { emsg, kopid } => {
|
||||||
|
let message = match kopid {
|
||||||
|
Some(k) => format!("An error occured - {} - {}", emsg, k),
|
||||||
|
None => format!("An error occured - {} - No Operation ID", emsg),
|
||||||
|
};
|
||||||
html! {
|
html! {
|
||||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
<h2>{ "Security" }</h2>
|
{ message }
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_ => html! { <></> },
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||||
|
<h2>{ "Security" }</h2>
|
||||||
|
</div>
|
||||||
|
{ error }
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
<button type="button" class="btn btn-primary"
|
||||||
|
disabled={ !submit_enabled }
|
||||||
|
onclick={
|
||||||
|
ctx.link().callback(|e| {
|
||||||
|
Msg::RequestCredentialUpdate
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{ "Password and Authentication Settings" }
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SecurityApp {
|
||||||
|
async fn fetch_token_valid(id: String, token: String) -> Result<Msg, FetchError> {
|
||||||
|
let mut opts = RequestInit::new();
|
||||||
|
opts.method("GET");
|
||||||
|
opts.mode(RequestMode::SameOrigin);
|
||||||
|
|
||||||
|
let uri = format!("/v1/account/{}/_credential/_update", id);
|
||||||
|
|
||||||
|
let request = Request::new_with_str_and_init(uri.as_str(), &opts)?;
|
||||||
|
|
||||||
|
request
|
||||||
|
.headers()
|
||||||
|
.set("content-type", "application/json")
|
||||||
|
.expect_throw("failed to set header");
|
||||||
|
request
|
||||||
|
.headers()
|
||||||
|
.set("authorization", format!("Bearer {}", token).as_str())
|
||||||
|
.expect_throw("failed to set header");
|
||||||
|
|
||||||
|
let window = utils::window();
|
||||||
|
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
|
||||||
|
let resp: Response = resp_value.dyn_into().expect_throw("Invalid response type");
|
||||||
|
let status = resp.status();
|
||||||
|
|
||||||
|
if status == 200 {
|
||||||
|
let jsval = JsFuture::from(resp.json()?).await?;
|
||||||
|
let (token, status): (CUSessionToken, CUStatus) =
|
||||||
|
jsval.into_serde().expect_throw("Invalid response type");
|
||||||
|
Ok(Msg::BeginCredentialUpdate { token, status })
|
||||||
|
} else {
|
||||||
|
let headers = resp.headers();
|
||||||
|
let kopid = headers.get("x-kanidm-opid").ok().flatten();
|
||||||
|
let text = JsFuture::from(resp.text()?).await?;
|
||||||
|
let emsg = text.as_string().unwrap_or_else(|| "".to_string());
|
||||||
|
// let jsval_json = JsFuture::from(resp.json()?).await?;
|
||||||
|
Ok(Msg::Error { emsg, kopid })
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
7
kanidmd_web_ui/src/wasmloader.js
Normal file
7
kanidmd_web_ui/src/wasmloader.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
// loads the module which loads the WASM. It's loaders all the way down.
|
||||||
|
import init, { run_app } from '/pkg/kanidmd_web_ui.js';
|
||||||
|
async function main() {
|
||||||
|
await init('/pkg/kanidmd_web_ui_bg.wasm');
|
||||||
|
run_app();
|
||||||
|
}
|
||||||
|
main()
|
Loading…
Reference in a new issue