383 170 164 authentication updates - credential update webui! (#809)

This commit is contained in:
Firstyear 2022-06-05 16:30:08 +10:00 committed by GitHub
parent 0d510baebb
commit b97d13d284
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 2588 additions and 482 deletions

38
Cargo.lock generated
View file

@ -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"

View file

@ -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

View file

@ -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,

View file

@ -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 }

View file

@ -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);
} }

View file

@ -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}"

View 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);

View file

@ -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

View file

@ -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

View file

@ -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),

View file

@ -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();

View file

@ -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",

View file

@ -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

View file

@ -1,7 +1,10 @@
#!/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

View file

@ -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
View 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

View 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;

View file

@ -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
} }

View file

@ -0,0 +1,5 @@
export function modal_hide(m) {
var elem = document.getElementById(m);
var modal = bootstrap.Modal.getInstance(elem);
modal.hide();
}

View file

@ -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;
}

View 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>
}
}
}

View 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);
}
}

View file

@ -0,0 +1,6 @@
pub mod reset;
mod delete;
mod eventbus;
mod pwmodal;
mod totpmodal;

View 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>
}
}
}

View 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 })
}
}
}

View 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>
}
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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

View file

@ -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;

View file

@ -770,12 +770,12 @@ impl Component for LoginApp {
// May need to set these classes? // May need to set these classes?
// <body class="html-body form-body"> // <body class="html-body form-body">
html! { html! {
<main class="form-signin"> <main class="form-signin">
<div class="container"> <div class="container">
<h2>{ "Kanidm Alpha 🦀" }</h2> <h2>{ "Kanidm Alpha 🦀" }</h2>
</div> </div>
{ self.view_state(ctx) } { self.view_state(ctx) }
</main> </main>
} }
} }

View file

@ -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>

View file

@ -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()
}

View file

@ -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">

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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 })
}
}
} }

View file

@ -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
}
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
} }
} }
*/
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! {
<div class="alert alert-danger alert-dismissible fade show" role="alert">
{ message }
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
}
}
_ => html! { <></> },
};
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="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h2>{ "Security" }</h2> <h2>{ "Security" }</h2>
</div> </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 })
} }
} }
} }

View 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()