diff --git a/Cargo.lock b/Cargo.lock index ed6a2c058..94d67fd76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -601,21 +601,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "compact_jwt" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41e359e67b8bb65fdcdc67e506f65b4aec4a97c7e731f321ed1b4b86c443ca6e" -dependencies = [ - "base64 0.13.0", - "openssl", - "serde", - "serde_json", - "tracing", - "url", - "uuid", -] - [[package]] name = "compact_jwt" version = "0.2.0" @@ -1891,7 +1876,7 @@ dependencies = [ "async-trait", "base64 0.13.0", "chrono", - "compact_jwt 0.2.0", + "compact_jwt", "compiled-uuid", "concread", "criterion", @@ -1945,24 +1930,15 @@ dependencies = [ name = "kanidm_client" version = "1.1.0-alpha.7" dependencies = [ - "async-std", - "base64 0.13.0", - "compact_jwt 0.1.9", - "futures", - "kanidm", "kanidm_proto", - "oauth2", "reqwest", - "score", "serde", "serde_json", "tokio", "toml", "tracing", - "tracing-subscriber", "url", "uuid", - "webauthn-authenticator-rs", "webauthn-rs", ] @@ -1983,7 +1959,7 @@ dependencies = [ name = "kanidm_tools" version = "1.1.0-alpha.7" dependencies = [ - "compact_jwt 0.2.0", + "compact_jwt", "dialoguer", "kanidm_client", "kanidm_proto", @@ -3193,22 +3169,31 @@ version = "0.1.0" dependencies = [ "async-std", "async-trait", - "compact_jwt 0.2.0", + "base64 0.13.0", + "compact_jwt", + "futures", "futures-util", "kanidm", + "kanidm_client", "kanidm_proto", "ldap3_proto", "libc", + "oauth2", "openssl", "profiles", + "reqwest", "serde", + "serde_json", "tide", "tide-openssl", "tokio", "tokio-openssl", "tokio-util", "tracing", + "tracing-subscriber", + "url", "uuid", + "webauthn-authenticator-rs", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b9e6b5649..3cc599f77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ lto = "thin" [workspace] members = [ "kanidm_proto", - "kanidmd", + "kanidmd/idm", "kanidmd/score", "kanidmd/daemon", "kanidmd_web_ui", diff --git a/Makefile b/Makefile index 4a104a6f6..356d6ff3b 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ IMAGE_BASE ?= kanidm IMAGE_VERSION ?= devel EXT_OPTS ?= IMAGE_ARCH ?= "linux/amd64,linux/arm64" -ARGS ?= --build-arg "SCCACHE_REDIS=redis://172.24.20.4:6379" +ARGS ?= --build-arg "SCCACHE_REDIS=redis://redis.dev.blackhats.net.au:6379" BOOK_VERSION ?= master diff --git a/kanidm_client/Cargo.toml b/kanidm_client/Cargo.toml index 99329025b..fa8c0b323 100644 --- a/kanidm_client/Cargo.toml +++ b/kanidm_client/Cargo.toml @@ -22,15 +22,3 @@ url = { version = "2", features = ["serde"] } webauthn-rs = "0.3" tokio = { version = "1", features = ["rt", "net", "time", "macros", "sync", "signal"] } -[dev-dependencies] -tracing-subscriber = "0.3" -tokio = { version = "1", features = ["rt", "net", "time", "macros", "sync", "signal"] } -kanidm = { path = "../kanidmd" } -score = { path = "../kanidmd/score" } -futures = "0.3" -async-std = { version = "1.6", features = ["tokio1"] } - -webauthn-authenticator-rs = "0.3.0-alpha.12" -oauth2_ext = { package = "oauth2", version = "4.0", default-features = false } -base64 = "0.13" -compact_jwt = "^0.1.5" diff --git a/kanidm_proto/src/v1.rs b/kanidm_proto/src/v1.rs index fc8ee5bb9..091494233 100644 --- a/kanidm_proto/src/v1.rs +++ b/kanidm_proto/src/v1.rs @@ -58,6 +58,43 @@ pub enum ConsistencyError { BackendIndexSync, } +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "lowercase")] +pub enum PasswordFeedback { + // https://docs.rs/zxcvbn/latest/zxcvbn/feedback/enum.Suggestion.html + UseAFewWordsAvoidCommonPhrases, + NoNeedForSymbolsDigitsOrUppercaseLetters, + AddAnotherWordOrTwo, + CapitalizationDoesntHelpVeryMuch, + AllUppercaseIsAlmostAsEasyToGuessAsAllLowercase, + ReversedWordsArentMuchHarderToGuess, + PredictableSubstitutionsDontHelpVeryMuch, + UseALongerKeyboardPatternWithMoreTurns, + AvoidRepeatedWordsAndCharacters, + AvoidSequences, + AvoidRecentYears, + AvoidYearsThatAreAssociatedWithYou, + AvoidDatesAndYearsThatAreAssociatedWithYou, + // https://docs.rs/zxcvbn/latest/zxcvbn/feedback/enum.Warning.html + StraightRowsOfKeysAreEasyToGuess, + ShortKeyboardPatternsAreEasyToGuess, + RepeatsLikeAaaAreEasyToGuess, + RepeatsLikeAbcAbcAreOnlySlightlyHarderToGuess, + ThisIsATop10Password, + ThisIsATop100Password, + ThisIsACommonPassword, + ThisIsSimilarToACommonlyUsedPassword, + SequencesLikeAbcAreEasyToGuess, + RecentYearsAreEasyToGuess, + AWordByItselfIsEasyToGuess, + DatesAreOftenEasyToGuess, + NamesAndSurnamesByThemselvesAreEasyToGuess, + CommonNamesAndSurnamesAreEasyToGuess, + // Custom + TooShort(usize), + BadListed, +} + #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "lowercase")] pub enum OperationError { @@ -94,18 +131,17 @@ pub enum OperationError { SerdeCborError, AccessDenied, NotAuthenticated, + NotAuthorised, InvalidAuthState(String), InvalidSessionState, SystemProtectedObject, SystemProtectedAttribute, - PasswordTooWeak, - PasswordTooShort(usize), - PasswordEmpty, - PasswordBadListed, + PasswordQuality(Vec), CryptographyError, ResourceLimit, QueueDisconnected, Webauthn, + Wait(time::OffsetDateTime), } impl PartialEq for OperationError { diff --git a/kanidm_tools/src/cli/account.rs b/kanidm_tools/src/cli/account.rs index 3ef755bf2..943f6c7ee 100644 --- a/kanidm_tools/src/cli/account.rs +++ b/kanidm_tools/src/cli/account.rs @@ -12,7 +12,7 @@ use webauthn_authenticator_rs::{u2fhid::U2FHid, WebauthnAuthenticator}; use kanidm_client::ClientError; use kanidm_client::ClientError::Http as ClientErrorHttp; -use kanidm_proto::v1::OperationError::{PasswordBadListed, PasswordTooShort, PasswordTooWeak}; +use kanidm_proto::v1::OperationError::PasswordQuality; impl AccountOpt { pub fn debug(&self) -> bool { @@ -81,10 +81,10 @@ impl AccountOpt { ) { match e { // TODO: once the password length is configurable at a system level (#498), pull from the configuration. - ClientErrorHttp(_, Some(PasswordBadListed), _) => error!("Password is banned by the administrator of this system, please try a different one."), - ClientErrorHttp(_, Some(PasswordTooShort(pwminlength)), _) => error!("Password was too short (needs to be at least {} characters), please try again.", pwminlength), - ClientErrorHttp(_, Some(PasswordTooWeak), _) => error!("The supplied password did not match configured complexity requirements, please use a password with upper/lower case letters, symbols and digits."), - _ => error!("Error setting password -> {:?}", e) + ClientErrorHttp(_, Some(PasswordQuality(feedback)), _) => { + error!("{:?}", feedback) + } + _ => error!("Error setting password -> {:?}", e), } } } diff --git a/kanidm_unix_int/Cargo.toml b/kanidm_unix_int/Cargo.toml index ae221367b..486753205 100644 --- a/kanidm_unix_int/Cargo.toml +++ b/kanidm_unix_int/Cargo.toml @@ -45,7 +45,7 @@ path = "src/test_auth.rs" [dependencies] kanidm_client = { path = "../kanidm_client", version = "1.1.0-alpha" } kanidm_proto = { path = "../kanidm_proto", version = "1.1.0-alpha" } -kanidm = { path = "../kanidmd", version = "1.1.0-alpha" } +kanidm = { path = "../kanidmd/idm", version = "1.1.0-alpha" } tracing = "0.1" tracing-subscriber = "0.3" @@ -80,7 +80,7 @@ lru = "0.7" # default = [ "libsqlite3-sys/bundled" ] [dev-dependencies] -kanidm = { path = "../kanidmd" } +# kanidm = { path = "../kanidmd/idm" } score = { path = "../kanidmd/score" } [build-dependencies] diff --git a/kanidmd/daemon/Cargo.toml b/kanidmd/daemon/Cargo.toml index 4f3928bbc..0a0d3866c 100644 --- a/kanidmd/daemon/Cargo.toml +++ b/kanidmd/daemon/Cargo.toml @@ -17,7 +17,7 @@ name = "kanidmd" path = "src/main.rs" [dependencies] -kanidm = { path = "../" } +kanidm = { path = "../idm" } score = { path = "../score" } structopt = { version = "0.3", default-features = false } users = "0.11" diff --git a/kanidmd/daemon/build.rs b/kanidmd/daemon/build.rs index 7662cdb68..e5c75a8de 100644 --- a/kanidmd/daemon/build.rs +++ b/kanidmd/daemon/build.rs @@ -8,7 +8,7 @@ use structopt::StructOpt; use serde::{Deserialize, Serialize}; -include!("../src/lib/audit_loglevel.rs"); +include!("../idm/src/audit_loglevel.rs"); include!("src/opt.rs"); fn main() { diff --git a/kanidmd/Cargo.toml b/kanidmd/idm/Cargo.toml similarity index 94% rename from kanidmd/Cargo.toml rename to kanidmd/idm/Cargo.toml index 8133c5f57..e6805cf5d 100644 --- a/kanidmd/Cargo.toml +++ b/kanidmd/idm/Cargo.toml @@ -12,10 +12,10 @@ repository = "https://github.com/kanidm/kanidm/" [lib] name = "kanidm" -path = "src/lib/lib.rs" +path = "src/lib.rs" [dependencies] -kanidm_proto = { path = "../kanidm_proto", version = "1.1.0-alpha" } +kanidm_proto = { path = "../../kanidm_proto", version = "1.1.0-alpha" } tracing = { version = "0.1", features = ["attributes"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-serde = "0.1" @@ -97,7 +97,7 @@ criterion = { version = "0.3", features = ["html_reports"] } webauthn-authenticator-rs = "0.3.0-alpha.12" [build-dependencies] -profiles = { path = "../profiles" } +profiles = { path = "../../profiles" } [[bench]] name = "scaling_10k" diff --git a/kanidmd/benches/scaling_10k.rs b/kanidmd/idm/benches/scaling_10k.rs similarity index 87% rename from kanidmd/benches/scaling_10k.rs rename to kanidmd/idm/benches/scaling_10k.rs index c02de384f..1d201262e 100644 --- a/kanidmd/benches/scaling_10k.rs +++ b/kanidmd/idm/benches/scaling_10k.rs @@ -3,7 +3,6 @@ use criterion::{ }; use kanidm; -use kanidm::audit::AuditScope; use kanidm::entry::{Entry, EntryInit, EntryNew}; use kanidm::entry_init; use kanidm::idm::server::{IdmServer, IdmServerDelayed}; @@ -30,10 +29,7 @@ pub fn scaling_user_create_single(c: &mut Criterion) { for _i in 0..iters { kanidm::macros::run_idm_test_no_logging( - |_qs: &QueryServer, - idms: &IdmServer, - _idms_delayed: &IdmServerDelayed, - au: &mut AuditScope| { + |_qs: &QueryServer, idms: &IdmServer, _idms_delayed: &IdmServerDelayed| { let ct = duration_from_epoch_now(); let start = Instant::now(); @@ -50,10 +46,10 @@ pub fn scaling_user_create_single(c: &mut Criterion) { ("displayname", Value::new_utf8s(&name)) ); - let cr = idms_prox_write.qs_write.internal_create(au, vec![e1]); + let cr = idms_prox_write.qs_write.internal_create(vec![e1]); assert!(cr.is_ok()); - idms_prox_write.commit(au).expect("Must not fail"); + idms_prox_write.commit().expect("Must not fail"); } elapsed = elapsed.checked_add(start.elapsed()).unwrap(); }, @@ -97,19 +93,16 @@ pub fn scaling_user_create_batched(c: &mut Criterion) { for _i in 0..iters { kanidm::macros::run_idm_test_no_logging( - |_qs: &QueryServer, - idms: &IdmServer, - _idms_delayed: &IdmServerDelayed, - au: &mut AuditScope| { + |_qs: &QueryServer, idms: &IdmServer, _idms_delayed: &IdmServerDelayed| { let ct = duration_from_epoch_now(); let start = Instant::now(); let idms_prox_write = task::block_on(idms.proxy_write_async(ct)); - let cr = idms_prox_write.qs_write.internal_create(au, data.clone()); + let cr = idms_prox_write.qs_write.internal_create(data.clone()); assert!(cr.is_ok()); - idms_prox_write.commit(au).expect("Must not fail"); + idms_prox_write.commit().expect("Must not fail"); elapsed = elapsed.checked_add(start.elapsed()).unwrap(); }, ); diff --git a/kanidmd/build.rs b/kanidmd/idm/build.rs similarity index 100% rename from kanidmd/build.rs rename to kanidmd/idm/build.rs diff --git a/kanidmd/src/lib/access.rs b/kanidmd/idm/src/access.rs similarity index 87% rename from kanidmd/src/lib/access.rs rename to kanidmd/idm/src/access.rs index 2b95a0e0c..1427dc699 100644 --- a/kanidmd/src/lib/access.rs +++ b/kanidmd/idm/src/access.rs @@ -367,6 +367,17 @@ impl AccessControlProfile { } } +#[derive(Debug, Clone, PartialEq)] +pub struct AccessEffectivePermission { + // I don't think we need this? The ident is implied by the requestor. + // ident: Uuid, + pub target: Uuid, + pub search: BTreeSet, + pub modify_pres: BTreeSet, + pub modify_rem: BTreeSet, + pub modify_class: BTreeSet, +} + // ========================================================================= // ACP transactions and management for server bits. // ========================================================================= @@ -400,8 +411,8 @@ pub trait AccessControlsTransaction<'a> { fn search_related_acp<'b>( &'b self, rec_entry: &Entry, - se: &SearchEvent, - ) -> Vec<&'b AccessControlSearch> { + ident: &Identity, + ) -> Vec<(&'b AccessControlSearch, Filter)> { let search_state = self.get_search(); // let acp_related_search_cache = self.get_acp_related_search_cache(); let acp_resolve_filter_cache = self.get_acp_resolve_filter_cache(); @@ -432,12 +443,11 @@ pub trait AccessControlsTransaction<'a> { } else { */ // else, we calculate this, and then stash/cache the uuids. - let related_acp: Vec<&AccessControlSearch> = + let related_acp: Vec<(&AccessControlSearch, Filter)> = spanned!("access::search_related_acp", { search_state .iter() - // .filter_map(|(_, acs)| { - .filter(|acs| { + .filter_map(|acs| { // Now resolve the receiver filter // Okay, so in filter resolution, the primary error case // is that we have a non-user in the event. We have already @@ -454,17 +464,35 @@ pub trait AccessControlsTransaction<'a> { // such that it takes an entry, rather than an event, but that // would create issues in search. match (&acs.acp.receiver).resolve( - &se.ident, + ident, None, Some(acp_resolve_filter_cache), ) { - Ok(f_res) => rec_entry.entry_match_no_index(&f_res), + Ok(f_res) => { + if rec_entry.entry_match_no_index(&f_res) { + // Now, for each of the acp's that apply to our receiver, resolve their + // related target filters. + (&acs.acp.targetscope) + .resolve(ident, None, Some(acp_resolve_filter_cache)) + .map_err(|e| { + admin_error!( + ?e, + "A internal filter/event was passed for resolution!?!?" + ); + e + }) + .ok() + .map(|f_res| (acs, f_res)) + } else { + None + } + } Err(e) => { admin_error!( ?e, "A internal filter/event was passed for resolution!?!?" ); - false + None } } }) @@ -501,25 +529,8 @@ pub trait AccessControlsTransaction<'a> { trace!(event = %se.ident, "Access check for search (filter) event"); // First get the set of acps that apply to this receiver - let related_acp = self.search_related_acp(rec_entry, se); - let acp_resolve_filter_cache = self.get_acp_resolve_filter_cache(); - - let related_acp: Vec<(&AccessControlSearch, _)> = related_acp - .into_iter() - .filter_map(|acs| { - (&acs.acp.targetscope) - .resolve(&se.ident, None, Some(acp_resolve_filter_cache)) - .map_err(|e| { - admin_error!( - ?e, - "A internal filter/event was passed for resolution!?!?" - ); - e - }) - .ok() - .map(|f_res| (acs, f_res)) - }) - .collect(); + let related_acp: Vec<(&AccessControlSearch, _)> = + self.search_related_acp(rec_entry, &se.ident); /* related_acp.iter().for_each(|racp| { @@ -619,37 +630,19 @@ pub trait AccessControlsTransaction<'a> { */ trace!("Access check for search (reduce) event: {}", se.ident); - let acp_resolve_filter_cache = self.get_acp_resolve_filter_cache(); // Get the relevant acps for this receiver. - let related_acp = self.search_related_acp(rec_entry, se); - - let related_acp: Vec<&AccessControlSearch> = if let Some(r_attrs) = se.attrs.as_ref() { - related_acp - .into_iter() - .filter(|acs| !acs.attrs.is_disjoint(r_attrs)) - .collect() - } else { - related_acp - }; - - // Compile all the target filters in one pass. - let related_acp: Vec<(&AccessControlSearch, _)> = related_acp - .into_iter() - .filter_map(|acs| { - (&acs.acp.targetscope) - .resolve(&se.ident, None, Some(acp_resolve_filter_cache)) - .map_err(|e| { - admin_error!( - ?e, - "A internal filter/event was passed for resolution!?!?" - ); - e - }) - .ok() - .map(|f_res| (acs, f_res)) - }) - .collect(); + let related_acp: Vec<(&AccessControlSearch, _)> = + self.search_related_acp(rec_entry, &se.ident); + let related_acp: Vec<(&AccessControlSearch, _)> = + if let Some(r_attrs) = se.attrs.as_ref() { + related_acp + .into_iter() + .filter(|(acs, _)| !acs.attrs.is_disjoint(r_attrs)) + .collect() + } else { + related_acp + }; /* related_acp.iter().for_each(|racp| { @@ -729,48 +722,28 @@ pub trait AccessControlsTransaction<'a> { }) } - #[allow(clippy::cognitive_complexity)] - fn modify_allow_operation( - &self, - me: &ModifyEvent, - entries: &[Arc], - ) -> Result { - let rec_entry: &Entry = match &me.ident.origin { - IdentType::Internal => { - trace!("Internal operation, bypassing access check"); - // No need to check ACS - return Ok(true); - } - IdentType::User(u) => &u.entry, - }; - spanned!("access::modify_allow_operation", { - trace!("Access check for modify event: {}", me.ident); + fn modify_related_acp<'b>( + &'b self, + rec_entry: &Entry, + ident: &Identity, + ) -> Vec<(&'b AccessControlModify, Filter)> { + // Some useful references we'll use for the remainder of the operation + let modify_state = self.get_modify(); + let acp_resolve_filter_cache = self.get_acp_resolve_filter_cache(); - // Some useful references we'll use for the remainder of the operation - let modify_state = self.get_modify(); - let acp_resolve_filter_cache = self.get_acp_resolve_filter_cache(); - - // Pre-check if the no-no purge class is present - let disallow = me - .modlist - .iter() - .any(|m| matches!(m, Modify::Purged(a) if a == "class")); - - if disallow { - security_access!("Disallowing purge class in modification"); - return Ok(false); - } - - // Find the acps that relate to the caller, and compile their related - // target filters. - let related_acp: Vec<(&AccessControlModify, _)> = modify_state + // Find the acps that relate to the caller, and compile their related + // target filters. + let related_acp: Vec<(&AccessControlModify, _)> = spanned!( + "access::modify_related_acp", + { + modify_state .iter() .filter_map(|acs| { - match (&acs.acp.receiver).resolve(&me.ident, None, Some(acp_resolve_filter_cache)) { + match (&acs.acp.receiver).resolve(ident, None, Some(acp_resolve_filter_cache)) { Ok(f_res) => { if rec_entry.entry_match_no_index(&f_res) { (&acs.acp.targetscope) - .resolve(&me.ident, None, Some(acp_resolve_filter_cache)) + .resolve(ident, None, Some(acp_resolve_filter_cache)) .map_err(|e| { admin_error!( "A internal filter/event was passed for resolution!?!? {:?}", @@ -793,7 +766,45 @@ pub trait AccessControlsTransaction<'a> { } } }) - .collect(); + .collect() + } + ); + + related_acp + } + + #[allow(clippy::cognitive_complexity)] + fn modify_allow_operation( + &self, + me: &ModifyEvent, + entries: &[Arc], + ) -> Result { + let rec_entry: &Entry = match &me.ident.origin { + IdentType::Internal => { + trace!("Internal operation, bypassing access check"); + // No need to check ACS + return Ok(true); + } + IdentType::User(u) => &u.entry, + }; + spanned!("access::modify_allow_operation", { + trace!("Access check for modify event: {}", me.ident); + + // Pre-check if the no-no purge class is present + let disallow = me + .modlist + .iter() + .any(|m| matches!(m, Modify::Purged(a) if a == "class")); + + if disallow { + security_access!("Disallowing purge class in modification"); + return Ok(false); + } + + // Find the acps that relate to the caller, and compile their related + // target filters. + let related_acp: Vec<(&AccessControlModify, _)> = + self.modify_related_acp(&rec_entry, &me.ident); related_acp.iter().for_each(|racp| { trace!("Related acs -> {:?}", racp.0.acp.name); @@ -1137,6 +1148,143 @@ pub trait AccessControlsTransaction<'a> { Ok(r) }) } + + fn effective_permission_check( + &self, + ident: &Identity, + attrs: Option>, + entries: &[Arc], + ) -> Result, OperationError> { + // I think we need a structure like " CheckResult, which is in the order of the + // entries, but also stashes the uuid. Then it has search, mod, create, delete, + // as seperate attrs to describe what is capable. + + // Does create make sense here? I don't think it does. Create requires you to + // have an entry template. I think james was right about the create being + // a template copy op ... + + let rec_entry: &Entry = match &ident.origin { + IdentType::Internal => { + // In production we can't risk leaking data here, so we return + // empty sets. + security_critical!("IMPOSSIBLE STATE: Internal search in external interface?! Returning empty for safety."); + // No need to check ACS + return Err(OperationError::InvalidState); + } + IdentType::User(u) => &u.entry, + }; + + spanned!("access::effective_permission_check", { + trace!(ident = %ident, "Effective permission check"); + // I think we seperate this to multiple checks ...? + + // == search == + // Get the relevant acps for this receiver. + let search_related_acp: Vec<(&AccessControlSearch, _)> = + self.search_related_acp(rec_entry, ident); + let search_related_acp: Vec<(&AccessControlSearch, _)> = + if let Some(r_attrs) = attrs.as_ref() { + search_related_acp + .into_iter() + .filter(|(acs, _)| !acs.attrs.is_disjoint(r_attrs)) + .collect() + } else { + search_related_acp + }; + + /* + search_related_acp.iter().for_each(|(racp, _)| { + trace!("Related acs -> {:?}", racp.acp.name); + }); + */ + + // == modify == + + let modify_related_acp: Vec<(&AccessControlModify, _)> = + self.modify_related_acp(rec_entry, ident); + + /* + modify_related_acp.iter().for_each(|(racp, _)| { + trace!("Related acm -> {:?}", racp.acp.name); + }); + */ + + let effective_permissions: Vec<_> = entries + .iter() + .map(|e| { + // == search == + let allowed_attrs: BTreeSet = search_related_acp + .iter() + .filter_map(|(acs, f_res)| { + // if it applies + if e.entry_match_no_index(f_res) { + // security_access!(entry = ?e.get_uuid(), acs = %acs.acp.name, "entry matches acs"); + Some(acs.attrs.iter().map(|s| s.clone())) + } else { + trace!(entry = ?e.get_uuid(), acs = %acs.acp.name, "entry DOES NOT match acs"); // should this be `security_access`? + None + } + }) + .flatten() + .collect(); + + security_access!( + requested = ?attrs, + allows = ?allowed_attrs, + "attributes", + ); + + // intersect? + let search_effective = if let Some(r_attrs) = attrs.as_ref() { + r_attrs & &allowed_attrs + } else { + allowed_attrs + }; + + // == modify == + let modify_scoped_acp: Vec<&AccessControlModify> = modify_related_acp + .iter() + .filter_map(|(acm, f_res)| { + if e.entry_match_no_index(f_res) { + Some(*acm) + } else { + None + } + }) + .collect(); + + let modify_pres: BTreeSet = modify_scoped_acp + .iter() + .flat_map(|acp| acp.presattrs.iter().map(|v| v.clone())) + .collect(); + + let modify_rem: BTreeSet = modify_scoped_acp + .iter() + .flat_map(|acp| acp.remattrs.iter().map(|v| v.clone())) + .collect(); + + let modify_class: BTreeSet = modify_scoped_acp + .iter() + .flat_map(|acp| acp.classes.iter().map(|v| v.clone())) + .collect(); + + AccessEffectivePermission { + target: *e.get_uuid(), + search: search_effective, + modify_pres, + modify_rem, + modify_class, + } + }) + .collect(); + + effective_permissions.iter().for_each(|ep| { + trace!(?ep); + }); + + Ok(effective_permissions) + }) + } } pub struct AccessControlsWriteTransaction<'a> { @@ -1359,10 +1507,12 @@ impl AccessControls { mod tests { use crate::access::{ AccessControlCreate, AccessControlDelete, AccessControlModify, AccessControlProfile, - AccessControlSearch, AccessControls, AccessControlsTransaction, + AccessControlSearch, AccessControls, AccessControlsTransaction, AccessEffectivePermission, }; use crate::event::{CreateEvent, DeleteEvent, ModifyEvent, SearchEvent}; use crate::prelude::*; + use compiled_uuid::uuid; + use std::collections::BTreeSet; use std::sync::Arc; macro_rules! acp_from_entry_err { @@ -2332,4 +2482,110 @@ mod tests { // Test reject delete test_acp_delete!(&de_anon, vec![acp], &r_set, false); } + + macro_rules! test_acp_effective_permissions { + ( + $ident:expr, + $attrs:expr, + $search_controls:expr, + $modify_controls:expr, + $entries:expr, + $expect:expr + ) => {{ + let ac = AccessControls::new(); + let mut acw = ac.write(); + acw.update_search($search_controls) + .expect("Failed to update"); + acw.update_modify($modify_controls) + .expect("Failed to update"); + let acw = acw; + + let res = acw + .effective_permission_check($ident, $attrs, $entries) + .expect("Failed to apply effective_permission_check"); + + debug!("result --> {:?}", res); + debug!("expect --> {:?}", $expect); + // should be ok, and same as expect. + assert!(res == $expect); + }}; + } + + #[test] + fn test_access_effective_permission_check_1() { + let _ = crate::tracing_tree::test_init(); + + let admin = unsafe { Identity::from_impersonate_entry_ser(JSON_ADMIN_V1) }; + + let e1: Entry = Entry::unsafe_from_entry_str(JSON_TESTPERSON1); + let ev1 = unsafe { e1.into_sealed_committed() }; + + let r_set = vec![Arc::new(ev1.clone())]; + + test_acp_effective_permissions!( + &admin, + None, + vec![unsafe { + AccessControlSearch::from_raw( + "test_acp", + "d38640c4-0254-49f9-99b7-8ba7d0233f3d", + // apply to admin only + filter_valid!(f_eq("name", PartialValue::new_iname("admin"))), + // Allow admin to read only testperson1 + filter_valid!(f_eq("name", PartialValue::new_iname("testperson1"))), + // They can read "name". + "name", + ) + }], + vec![], + &r_set, + vec![AccessEffectivePermission { + target: uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"), + search: btreeset![AttrString::from("name")], + modify_pres: BTreeSet::new(), + modify_rem: BTreeSet::new(), + modify_class: BTreeSet::new(), + }] + ) + } + + #[test] + fn test_access_effective_permission_check_2() { + let _ = crate::tracing_tree::test_init(); + + let admin = unsafe { Identity::from_impersonate_entry_ser(JSON_ADMIN_V1) }; + + let e1: Entry = Entry::unsafe_from_entry_str(JSON_TESTPERSON1); + let ev1 = unsafe { e1.into_sealed_committed() }; + + let r_set = vec![Arc::new(ev1.clone())]; + + test_acp_effective_permissions!( + &admin, + None, + vec![], + vec![unsafe { + AccessControlModify::from_raw( + "test_acp", + "d38640c4-0254-49f9-99b7-8ba7d0233f3d", + // apply to admin only + filter_valid!(f_eq("name", PartialValue::new_iname("admin"))), + // Allow admin to read only testperson1 + filter_valid!(f_eq("name", PartialValue::new_iname("testperson1"))), + // They can read "name". + "name", + "name", + "object", + ) + }], + &r_set, + vec![AccessEffectivePermission { + target: uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"), + search: BTreeSet::new(), + modify_pres: btreeset![AttrString::from("name")], + modify_rem: btreeset![AttrString::from("name")], + modify_class: btreeset![AttrString::from("object")], + }] + ) + } } diff --git a/kanidmd/src/lib/actors/mod.rs b/kanidmd/idm/src/actors/mod.rs similarity index 100% rename from kanidmd/src/lib/actors/mod.rs rename to kanidmd/idm/src/actors/mod.rs diff --git a/kanidmd/src/lib/actors/v1_read.rs b/kanidmd/idm/src/actors/v1_read.rs similarity index 100% rename from kanidmd/src/lib/actors/v1_read.rs rename to kanidmd/idm/src/actors/v1_read.rs diff --git a/kanidmd/src/lib/actors/v1_write.rs b/kanidmd/idm/src/actors/v1_write.rs similarity index 100% rename from kanidmd/src/lib/actors/v1_write.rs rename to kanidmd/idm/src/actors/v1_write.rs diff --git a/kanidmd/src/lib/audit.rs b/kanidmd/idm/src/audit.rs similarity index 100% rename from kanidmd/src/lib/audit.rs rename to kanidmd/idm/src/audit.rs diff --git a/kanidmd/src/lib/audit_loglevel.rs b/kanidmd/idm/src/audit_loglevel.rs similarity index 100% rename from kanidmd/src/lib/audit_loglevel.rs rename to kanidmd/idm/src/audit_loglevel.rs diff --git a/kanidmd/src/lib/be/dbentry.rs b/kanidmd/idm/src/be/dbentry.rs similarity index 100% rename from kanidmd/src/lib/be/dbentry.rs rename to kanidmd/idm/src/be/dbentry.rs diff --git a/kanidmd/src/lib/be/dbvalue.rs b/kanidmd/idm/src/be/dbvalue.rs similarity index 93% rename from kanidmd/src/lib/be/dbvalue.rs rename to kanidmd/idm/src/be/dbvalue.rs index da58c00a8..6e44d486b 100644 --- a/kanidmd/src/lib/be/dbvalue.rs +++ b/kanidmd/idm/src/be/dbvalue.rs @@ -30,6 +30,16 @@ impl std::fmt::Debug for DbPasswordV1 { } } +#[derive(Serialize, Deserialize, Debug)] +pub enum DbValueIntentTokenStateV1 { + #[serde(rename = "v")] + Valid, + #[serde(rename = "p")] + InProgress(Uuid, Duration), + #[serde(rename = "c")] + Consumed, +} + #[derive(Serialize, Deserialize, Debug)] pub enum DbTotpAlgoV1 { S1, @@ -231,6 +241,15 @@ pub enum DbValueV1 { PublicBinary(String, Vec), #[serde(rename = "RS")] RestrictedString(String), + #[serde(rename = "IT")] + IntentToken { + u: Uuid, + s: DbValueIntentTokenStateV1, + }, + #[serde(rename = "TE")] + TrustedDeviceEnrollment { u: Uuid }, + #[serde(rename = "AS")] + AuthSession { u: Uuid }, } #[cfg(test)] diff --git a/kanidmd/src/lib/be/idl_arc_sqlite.rs b/kanidmd/idm/src/be/idl_arc_sqlite.rs similarity index 100% rename from kanidmd/src/lib/be/idl_arc_sqlite.rs rename to kanidmd/idm/src/be/idl_arc_sqlite.rs diff --git a/kanidmd/src/lib/be/idl_sqlite.rs b/kanidmd/idm/src/be/idl_sqlite.rs similarity index 100% rename from kanidmd/src/lib/be/idl_sqlite.rs rename to kanidmd/idm/src/be/idl_sqlite.rs diff --git a/kanidmd/src/lib/be/idxkey.rs b/kanidmd/idm/src/be/idxkey.rs similarity index 100% rename from kanidmd/src/lib/be/idxkey.rs rename to kanidmd/idm/src/be/idxkey.rs diff --git a/kanidmd/src/lib/be/mod.rs b/kanidmd/idm/src/be/mod.rs similarity index 100% rename from kanidmd/src/lib/be/mod.rs rename to kanidmd/idm/src/be/mod.rs diff --git a/kanidmd/src/lib/config.rs b/kanidmd/idm/src/config.rs similarity index 100% rename from kanidmd/src/lib/config.rs rename to kanidmd/idm/src/config.rs diff --git a/kanidmd/src/lib/constants/acp.rs b/kanidmd/idm/src/constants/acp.rs similarity index 99% rename from kanidmd/src/lib/constants/acp.rs rename to kanidmd/idm/src/constants/acp.rs index d071fa86e..cc8ca53fd 100644 --- a/kanidmd/src/lib/constants/acp.rs +++ b/kanidmd/idm/src/constants/acp.rs @@ -112,7 +112,7 @@ pub const JSON_IDM_SELF_ACP_WRITE_V1: &str = r#"{ "uuid": ["00000000-0000-0000-0000-ffffff000021"], "description": ["Builtin IDM Control for self write - required for people to update their own identities and credentials in line with best practices."], "acp_receiver": [ - "{\"and\": [\"self\", {\"andnot\": {\"or\": [{\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}, {\"eq\": [\"uuid\", \"00000000-0000-0000-0000-ffffffffffff\"]}]}}]}" + "{\"and\": [\"self\", {\"eq\": [\"class\", \"person\"]}, {\"eq\": [\"class\", \"account\"]}, {\"andnot\": {\"or\": [{\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}, {\"eq\": [\"uuid\", \"00000000-0000-0000-0000-ffffffffffff\"]}]}}]}" ], "acp_targetscope": [ "{\"and\": [{\"eq\": [\"class\",\"person\"]}, {\"eq\": [\"class\",\"account\"]}, \"self\"]}" diff --git a/kanidmd/src/lib/constants/entries.rs b/kanidmd/idm/src/constants/entries.rs similarity index 100% rename from kanidmd/src/lib/constants/entries.rs rename to kanidmd/idm/src/constants/entries.rs diff --git a/kanidmd/src/lib/constants/mod.rs b/kanidmd/idm/src/constants/mod.rs similarity index 100% rename from kanidmd/src/lib/constants/mod.rs rename to kanidmd/idm/src/constants/mod.rs diff --git a/kanidmd/src/lib/constants/rockyou_3_10.json b/kanidmd/idm/src/constants/rockyou_3_10.json similarity index 100% rename from kanidmd/src/lib/constants/rockyou_3_10.json rename to kanidmd/idm/src/constants/rockyou_3_10.json diff --git a/kanidmd/src/lib/constants/rockyou_popular10m_3_10.json b/kanidmd/idm/src/constants/rockyou_popular10m_3_10.json similarity index 100% rename from kanidmd/src/lib/constants/rockyou_popular10m_3_10.json rename to kanidmd/idm/src/constants/rockyou_popular10m_3_10.json diff --git a/kanidmd/src/lib/constants/schema.rs b/kanidmd/idm/src/constants/schema.rs similarity index 97% rename from kanidmd/src/lib/constants/schema.rs rename to kanidmd/idm/src/constants/schema.rs index b78108903..f5c518de8 100644 --- a/kanidmd/src/lib/constants/schema.rs +++ b/kanidmd/idm/src/constants/schema.rs @@ -836,6 +836,35 @@ pub const JSON_SCHEMA_ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE: &str = r#"{ } }"#; +pub const JSON_SCHEMA_ATTR_CREDENTIAL_UPDATE_INTENT_TOKEN: &str = r#"{ + "attrs": { + "class": [ + "object", + "system", + "attributetype" + ], + "description": [ + "The status of a credential update intent token" + ], + "index": [], + "unique": [ + "false" + ], + "multivalue": [ + "true" + ], + "attributename": [ + "credential_update_intent_token" + ], + "syntax": [ + "INTENT_TOKEN" + ], + "uuid": [ + "00000000-0000-0000-0000-ffff00000096" + ] + } +}"#; + // === classes === pub const JSON_SCHEMA_CLASS_PERSON: &str = r#" @@ -939,6 +968,7 @@ pub const JSON_SCHEMA_CLASS_ACCOUNT: &str = r#" ], "systemmay": [ "primary_credential", + "credential_update_intent_token", "ssh_publickey", "radius_secret", "account_expire", diff --git a/kanidmd/src/lib/constants/system_config.rs b/kanidmd/idm/src/constants/system_config.rs similarity index 100% rename from kanidmd/src/lib/constants/system_config.rs rename to kanidmd/idm/src/constants/system_config.rs diff --git a/kanidmd/src/lib/constants/uuids.rs b/kanidmd/idm/src/constants/uuids.rs similarity index 99% rename from kanidmd/src/lib/constants/uuids.rs rename to kanidmd/idm/src/constants/uuids.rs index 28e5b86da..41f70fc1d 100644 --- a/kanidmd/src/lib/constants/uuids.rs +++ b/kanidmd/idm/src/constants/uuids.rs @@ -165,6 +165,8 @@ pub const _UUID_SCHEMA_ATTR_RS256_PRIVATE_KEY_DER: Uuid = pub const _UUID_SCHEMA_CLASS_ORGPERSON: Uuid = uuid!("00000000-0000-0000-0000-ffff00000094"); pub const UUID_SCHEMA_ATTR_FERNET_PRIVATE_KEY_STR: Uuid = uuid!("00000000-0000-0000-0000-ffff00000095"); +pub const _UUID_SCHEMA_ATTR_CREDENTIAL_UPDATE_INTENT_TOKEN: Uuid = + uuid!("00000000-0000-0000-0000-ffff00000096"); // System and domain infos // I'd like to strongly criticise william of the past for making poor choices about these allocations. diff --git a/kanidmd/src/lib/credential/mod.rs b/kanidmd/idm/src/credential/mod.rs similarity index 99% rename from kanidmd/src/lib/credential/mod.rs rename to kanidmd/idm/src/credential/mod.rs index bdf598f38..3062b8f2a 100644 --- a/kanidmd/src/lib/credential/mod.rs +++ b/kanidmd/idm/src/credential/mod.rs @@ -308,7 +308,7 @@ impl From<&Credential> for CredentialDetail { CredentialDetailType::PasswordMfa( totp.is_some(), labels, - backup_code.iter().count(), + backup_code.as_ref().map(|c| c.code_set.len()).unwrap_or(0), ) } }, diff --git a/kanidmd/src/lib/credential/policy.rs b/kanidmd/idm/src/credential/policy.rs similarity index 100% rename from kanidmd/src/lib/credential/policy.rs rename to kanidmd/idm/src/credential/policy.rs diff --git a/kanidmd/src/lib/credential/softlock.rs b/kanidmd/idm/src/credential/softlock.rs similarity index 100% rename from kanidmd/src/lib/credential/softlock.rs rename to kanidmd/idm/src/credential/softlock.rs diff --git a/kanidmd/src/lib/credential/totp.rs b/kanidmd/idm/src/credential/totp.rs similarity index 100% rename from kanidmd/src/lib/credential/totp.rs rename to kanidmd/idm/src/credential/totp.rs diff --git a/kanidmd/src/lib/credential/webauthn.rs b/kanidmd/idm/src/credential/webauthn.rs similarity index 100% rename from kanidmd/src/lib/credential/webauthn.rs rename to kanidmd/idm/src/credential/webauthn.rs diff --git a/kanidmd/src/lib/crypto.rs b/kanidmd/idm/src/crypto.rs similarity index 100% rename from kanidmd/src/lib/crypto.rs rename to kanidmd/idm/src/crypto.rs diff --git a/kanidmd/src/lib/entry.rs b/kanidmd/idm/src/entry.rs similarity index 99% rename from kanidmd/src/lib/entry.rs rename to kanidmd/idm/src/entry.rs index e1a56c956..b2231bb8a 100644 --- a/kanidmd/src/lib/entry.rs +++ b/kanidmd/idm/src/entry.rs @@ -32,7 +32,7 @@ use crate::prelude::*; use crate::repl::cid::Cid; use crate::schema::{SchemaAttribute, SchemaClass, SchemaTransaction}; use crate::value::{IndexType, SyntaxType}; -use crate::value::{PartialValue, Value}; +use crate::value::{IntentTokenState, PartialValue, Value}; use crate::valueset::ValueSet; use kanidm_proto::v1::Entry as ProtoEntry; use kanidm_proto::v1::Filter as ProtoFilter; @@ -1605,6 +1605,14 @@ impl Entry { self.attrs.get(attr).and_then(|vs| vs.as_oauthscopemap()) } + #[inline(always)] + pub fn get_ava_as_intenttokens( + &self, + attr: &str, + ) -> Option<&std::collections::BTreeMap> { + self.attrs.get(attr).and_then(|vs| vs.as_intenttoken()) + } + #[inline(always)] /// If possible, return an iterator over the set of values transformed into a `&str`. pub fn get_ava_as_str(&self, attr: &str) -> Option> { @@ -1795,6 +1803,7 @@ impl Entry { #[inline(always)] /// Test if the following filter applies to and matches this entry. pub fn entry_match_no_index(&self, filter: &Filter) -> bool { + let _entered = trace_span!("entry::entry_match_no_index").entered(); self.entry_match_no_index_inner(filter.to_inner()) } diff --git a/kanidmd/src/lib/event.rs b/kanidmd/idm/src/event.rs similarity index 96% rename from kanidmd/src/lib/event.rs rename to kanidmd/idm/src/event.rs index f6d088ca6..2a839fb2b 100644 --- a/kanidmd/src/lib/event.rs +++ b/kanidmd/idm/src/event.rs @@ -748,6 +748,22 @@ impl AuthEventStep { cred: AuthCredential::Password(pw.to_string()), }) } + + #[cfg(test)] + pub fn cred_step_totp(sid: Uuid, totp: u32) -> Self { + AuthEventStep::Cred(AuthEventStepCred { + sessionid: sid, + cred: AuthCredential::Totp(totp), + }) + } + + #[cfg(test)] + pub fn cred_step_backup_code(sid: Uuid, code: &str) -> Self { + AuthEventStep::Cred(AuthEventStepCred { + sessionid: sid, + cred: AuthCredential::BackupCode(code.to_string()), + }) + } } #[derive(Debug)] @@ -804,6 +820,22 @@ impl AuthEvent { step: AuthEventStep::cred_step_password(sid, pw), } } + + #[cfg(test)] + pub fn cred_step_totp(sid: Uuid, totp: u32) -> Self { + AuthEvent { + ident: None, + step: AuthEventStep::cred_step_totp(sid, totp), + } + } + + #[cfg(test)] + pub fn cred_step_backup_code(sid: Uuid, code: &str) -> Self { + AuthEvent { + ident: None, + step: AuthEventStep::cred_step_backup_code(sid, code), + } + } } // Probably should be a struct with the session id present. diff --git a/kanidmd/src/lib/filter.rs b/kanidmd/idm/src/filter.rs similarity index 100% rename from kanidmd/src/lib/filter.rs rename to kanidmd/idm/src/filter.rs diff --git a/kanidmd/src/lib/identity.rs b/kanidmd/idm/src/identity.rs similarity index 100% rename from kanidmd/src/lib/identity.rs rename to kanidmd/idm/src/identity.rs diff --git a/kanidmd/src/lib/idm/account.rs b/kanidmd/idm/src/idm/account.rs similarity index 97% rename from kanidmd/src/lib/idm/account.rs rename to kanidmd/idm/src/idm/account.rs index 0d3047121..0865f4416 100644 --- a/kanidmd/src/lib/idm/account.rs +++ b/kanidmd/idm/src/idm/account.rs @@ -12,8 +12,9 @@ use crate::credential::totp::Totp; use crate::credential::{softlock::CredSoftLockPolicy, Credential}; use crate::idm::group::Group; use crate::modify::{ModifyInvalid, ModifyList}; -use crate::value::{PartialValue, Value}; +use crate::value::{IntentTokenState, PartialValue, Value}; +use std::collections::BTreeMap; use std::time::Duration; use time::OffsetDateTime; use uuid::Uuid; @@ -76,6 +77,11 @@ macro_rules! try_from_entry { let uuid = $value.get_uuid().clone(); + let credential_update_intent_tokens = $value + .get_ava_as_intenttokens("credential_update_intent_token") + .cloned() + .unwrap_or_else(|| BTreeMap::new()); + Ok(Account { uuid, name, @@ -88,6 +94,7 @@ macro_rules! try_from_entry { spn, mail_primary, mail, + credential_update_intent_tokens, }) }}; } @@ -110,14 +117,12 @@ pub(crate) struct Account { pub valid_from: Option, pub expire: Option, pub radius_secret: Option, - // primary: Credential - // app_creds: Vec - // account expiry? (as opposed to cred expiry) pub spn: String, // TODO #256: When you add mail, you should update the check to zxcvbn // to include these. pub mail_primary: Option, pub mail: Vec, + pub credential_update_intent_tokens: BTreeMap, } impl Account { diff --git a/kanidmd/src/lib/idm/authsession.rs b/kanidmd/idm/src/idm/authsession.rs similarity index 99% rename from kanidmd/src/lib/idm/authsession.rs rename to kanidmd/idm/src/idm/authsession.rs index 4eb372a14..0be02b744 100644 --- a/kanidmd/src/lib/idm/authsession.rs +++ b/kanidmd/idm/src/idm/authsession.rs @@ -585,6 +585,7 @@ impl AuthSession { // of what's next, or ordering. let valid_mechs = auth_session.valid_auth_mechs(); + security_info!(?valid_mechs, "Offering auth mechanisms"); let as_state = AuthState::Choose(valid_mechs); (Some(auth_session), as_state) } @@ -629,6 +630,7 @@ impl AuthSession { let allowed: Vec<_> = allowed_handler.next_auth_allowed(); if allowed.is_empty() { + security_info!("Unable to negotiate credentials"); ( None, Err(OperationError::InvalidAuthState( @@ -642,6 +644,7 @@ impl AuthSession { ) } } else { + security_error!("Unable to select a credential for authentication"); ( Some(AuthSessionState::Denied(BAD_CREDENTIALS)), Ok(AuthState::Denied(BAD_CREDENTIALS.to_string())), diff --git a/kanidmd/idm/src/idm/credupdatesession.rs b/kanidmd/idm/src/idm/credupdatesession.rs new file mode 100644 index 000000000..db9e7eb75 --- /dev/null +++ b/kanidmd/idm/src/idm/credupdatesession.rs @@ -0,0 +1,1702 @@ +use crate::access::AccessControlsTransaction; +use crate::credential::{BackupCodes, Credential}; +use crate::idm::account::Account; +use crate::idm::server::IdmServerCredUpdateTransaction; +use crate::idm::server::IdmServerProxyWriteTransaction; +use crate::prelude::*; +use crate::value::IntentTokenState; +use hashbrown::HashSet; + +use crate::credential::totp::{Totp, TOTP_DEFAULT_STEP}; + +use kanidm_proto::v1::{CredentialDetail, PasswordFeedback, TotpSecret}; + +use crate::utils::{backup_code_from_random, uuid_from_duration}; + +use serde::{Deserialize, Serialize}; + +use std::sync::Arc; +use std::time::Duration; +use time::OffsetDateTime; + +use tokio::sync::Mutex; + +use core::ops::Deref; + +const MAXIMUM_CRED_UPDATE_TTL: Duration = Duration::from_secs(900); +const MAXIMUM_INTENT_TTL: Duration = Duration::from_secs(86400); +const MINIMUM_INTENT_TTL: Duration = MAXIMUM_CRED_UPDATE_TTL; + +#[derive(Debug)] +pub enum PasswordQuality { + TooShort(usize), + BadListed, + Feedback(Vec), +} + +#[derive(Serialize, Deserialize, Debug)] +struct CredentialUpdateIntentTokenInner { + pub sessionid: Uuid, + // Who is it targeting? + pub target: Uuid, + // Id of the intent, for checking if it's already been used against this user. + pub uuid: Uuid, + // How long is it valid for? + pub max_ttl: Duration, +} + +#[derive(Clone, Debug)] +pub struct CredentialUpdateIntentToken { + token_enc: String, +} + +#[derive(Serialize, Deserialize, Debug)] +struct CredentialUpdateSessionTokenInner { + pub sessionid: Uuid, + // How long is it valid for? + pub max_ttl: Duration, +} + +pub struct CredentialUpdateSessionToken { + token_enc: String, +} + +#[derive(Debug)] +enum MfaRegState { + None, + TotpInit(Totp), + TotpTryAgain(Totp), + TotpInvalidSha1(Totp), +} + +#[derive(Debug)] +pub(crate) struct CredentialUpdateSession { + // Current credentials - these are on the Account! + account: Account, + // + intent_token_id: Option, + // Acc policy + // The credentials as they are being updated + primary: Option, + + // Internal reg state. + mfaregstate: MfaRegState, + // trusted_devices: Map? + + // +} + +#[derive(Debug)] +enum MfaRegStateStatus { + // Nothing in progress. + None, + TotpCheck(TotpSecret), + TotpTryAgain, + TotpInvalidSha1, + BackupCodes(HashSet), +} + +#[derive(Debug)] +pub(crate) struct CredentialUpdateSessionStatus { + // spn: ? + // ttl: Duration, + // + can_commit: bool, + primary: Option, + // Any info the client needs about mfareg state. + mfaregstate: MfaRegStateStatus, +} + +impl From<&CredentialUpdateSession> for CredentialUpdateSessionStatus { + fn from(session: &CredentialUpdateSession) -> Self { + CredentialUpdateSessionStatus { + can_commit: true, + primary: session.primary.as_ref().map(|c| c.into()), + mfaregstate: match &session.mfaregstate { + MfaRegState::None => MfaRegStateStatus::None, + MfaRegState::TotpInit(token) => MfaRegStateStatus::TotpCheck( + token.to_proto(session.account.name.as_str(), session.account.spn.as_str()), + ), + MfaRegState::TotpTryAgain(_) => MfaRegStateStatus::TotpTryAgain, + MfaRegState::TotpInvalidSha1(_) => MfaRegStateStatus::TotpInvalidSha1, + }, + } + } +} + +pub(crate) type CredentialUpdateSessionMutex = Arc>; + +pub struct InitCredentialUpdateIntentEvent { + // Who initiated this? + pub ident: Identity, + // Who is it targetting? + pub target: Uuid, + // How long is it valid for? + pub max_ttl: Duration, +} + +impl InitCredentialUpdateIntentEvent { + #[cfg(test)] + pub fn new_impersonate_entry( + e: std::sync::Arc>, + target: Uuid, + max_ttl: Duration, + ) -> Self { + let ident = Identity::from_impersonate_entry(e); + InitCredentialUpdateIntentEvent { + ident, + target, + max_ttl, + } + } +} + +pub struct InitCredentialUpdateEvent { + pub ident: Identity, + pub target: Uuid, +} + +impl InitCredentialUpdateEvent { + #[cfg(test)] + pub fn new_impersonate_entry(e: std::sync::Arc>) -> Self { + let ident = Identity::from_impersonate_entry(e); + let target = ident + .get_uuid() + .ok_or(OperationError::InvalidState) + .expect("Identity has no uuid associated"); + InitCredentialUpdateEvent { ident, target } + } +} + +impl<'a> IdmServerProxyWriteTransaction<'a> { + fn validate_init_credential_update( + &mut self, + target: Uuid, + ident: &Identity, + ) -> Result { + let entry = self.qs_write.internal_search_uuid(&target)?; + + security_info!( + ?entry, + %target, + "Initiating Credential Update Session", + ); + + // Is target an account? This checks for us. + let account = Account::try_from_entry_rw(entry.as_ref(), &mut self.qs_write)?; + + let effective_perms = self + .qs_write + .get_accesscontrols() + .effective_permission_check( + &ident, + Some(btreeset![AttrString::from("primary_credential")]), + &[entry], + )?; + + let eperm = effective_perms.get(0).ok_or_else(|| { + admin_error!("Effective Permission check returned no results"); + OperationError::InvalidState + })?; + + // Does the ident have permission to modify AND search the user-credentials of the target, given + // the current status of it's authentication? + + if eperm.target != account.uuid { + admin_error!("Effective Permission check target differs from requested entry uuid"); + return Err(OperationError::InvalidEntryState); + } + + if !eperm.search.contains("primary_credential") + || !eperm.modify_pres.contains("primary_credential") + || !eperm.modify_rem.contains("primary_credential") + { + security_info!( + "Requestor {} does not have permission to update credentials of {}", + ident, + account.spn + ); + return Err(OperationError::NotAuthorised); + } + + Ok(account) + } + + fn create_credupdate_session( + &mut self, + sessionid: Uuid, + intent_token_id: Option, + account: Account, + ct: Duration, + ) -> Result { + // - stash the current state of all associated credentials + let primary = account.primary.clone(); + // - store account policy (if present) + + let session = Arc::new(Mutex::new(CredentialUpdateSession { + account, + intent_token_id, + primary, + mfaregstate: MfaRegState::None, + })); + + let max_ttl = ct + MAXIMUM_CRED_UPDATE_TTL; + + let token = CredentialUpdateSessionTokenInner { sessionid, max_ttl }; + + let token_data = serde_json::to_vec(&token).map_err(|e| { + admin_error!(err = ?e, "Unable to encode token data"); + OperationError::SerdeJsonError + })?; + + let token_enc = self.token_enc_key.encrypt(&token_data); + + // Point of no return + + // Store the update session into the map. + self.cred_update_sessions.insert(sessionid, session); + + // - issue the CredentialUpdateToken (enc) + Ok(CredentialUpdateSessionToken { token_enc }) + } + + pub fn init_credential_update_intent( + &mut self, + event: &InitCredentialUpdateIntentEvent, + ct: Duration, + ) -> Result { + spanned!("idm::server::credupdatesession", { + let _account = self.validate_init_credential_update(event.target, &event.ident)?; + + // ==== AUTHORISATION CHECKED === + + // Build the intent token. + + // States For the user record + // - Initial (Valid) + // - Processing (Uuid of in flight req, plus the expiry time of the in flight) + // - Canceled (Back to Valid) + // - Complete (The credential was updatded). + + // We need to actually submit a mod to the user. + + let max_ttl = ct + event.max_ttl.clamp(MINIMUM_INTENT_TTL, MAXIMUM_INTENT_TTL); + let sessionid = uuid_from_duration(max_ttl, self.sid); + let uuid = Uuid::new_v4(); + + let target = event.target; + + let token = CredentialUpdateIntentTokenInner { + sessionid, + target, + uuid, + max_ttl, + }; + + let token_data = serde_json::to_vec(&token).map_err(|e| { + admin_error!(err = ?e, "Unable to encode token data"); + OperationError::SerdeJsonError + })?; + + let token_enc = self + .token_enc_key + .encrypt_at_time(&token_data, ct.as_secs()); + + Ok(CredentialUpdateIntentToken { token_enc }) + }) + } + + pub fn exchange_intent_credential_update( + &mut self, + token: CredentialUpdateIntentToken, + ct: Duration, + ) -> Result { + let token: CredentialUpdateIntentTokenInner = self + .token_enc_key + .decrypt(&token.token_enc) + .map_err(|e| { + admin_error!(?e, "Failed to decrypt intent request"); + OperationError::SessionExpired + }) + .and_then(|data| { + serde_json::from_slice(&data).map_err(|e| { + admin_error!(err = ?e, "Failed to deserialise intent request"); + OperationError::SerdeJsonError + }) + })?; + + // Check the TTL + if ct >= token.max_ttl { + trace!(?ct, ?token.max_ttl); + security_info!(%token.sessionid, "session expired"); + return Err(OperationError::SessionExpired); + } + + let entry = self.qs_write.internal_search_uuid(&token.target)?; + + // Is target an account? This checks for us. + let account = Account::try_from_entry_rw(entry.as_ref(), &mut self.qs_write)?; + + // Check there is not already a user session in progress with this intent token. + // Is there a need to revoke intent tokens? + + match account + .credential_update_intent_tokens + .get(&token.sessionid) + { + Some(IntentTokenState::Consumed) => { + security_info!( + ?entry, + %token.target, + "Rejecting Update Session - Intent Token has already been exchanged", + ); + return Err(OperationError::SessionExpired); + } + Some(IntentTokenState::InProgress(si, sd)) => { + if ct > *sd { + // The former session has expired, continue. + security_info!( + ?entry, + %token.target, + "Initiating Credential Update Session - Previous session {} has expired", si + ); + } else { + security_info!( + ?entry, + %token.target, + "Rejecting Update Session - Intent Token is in use {}. Try again later", si + ); + return Err(OperationError::Wait(OffsetDateTime::unix_epoch() + *sd)); + } + } + Some(IntentTokenState::Valid) | None => { + security_info!( + ?entry, + %token.target, + "Initiating Credential Update Session", + ); + } + }; + + // To prevent issues with repl, we need to associate this cred update session id, with + // this intent token id. + + // Store the intent id in the session (if needed) so that we can check the state at the + // end of the update. + + // We need to pin the id from the intent token into the credential to ensure it's not re-used + + // Need to change this to the expiry time, so we can purge up to. + let sessionid = uuid_from_duration(ct + MAXIMUM_CRED_UPDATE_TTL, self.sid); + + let modlist = ModifyList::new_append( + "credential_update_intent_token", + Value::IntentToken( + token.sessionid, + IntentTokenState::InProgress(sessionid, ct + MAXIMUM_CRED_UPDATE_TTL), + ), + ); + + self.qs_write + .internal_modify( + // Filter as executed + &filter!(f_eq("uuid", PartialValue::new_uuidr(&account.uuid))), + &modlist, + ) + .map_err(|e| { + request_error!(error = ?e); + e + })?; + + // ========== + // Okay, good to exchange. + + self.create_credupdate_session(sessionid, Some(token.sessionid), account, ct) + } + + pub fn init_credential_update( + &mut self, + event: &InitCredentialUpdateEvent, + ct: Duration, + ) -> Result { + spanned!("idm::server::credupdatesession", { + let account = self.validate_init_credential_update(event.target, &event.ident)?; + + // ==== AUTHORISATION CHECKED === + + // This is the expiry time, so that our cleanup task can "purge up to now" rather + // than needing to do calculations. + let sessionid = uuid_from_duration(ct + MAXIMUM_CRED_UPDATE_TTL, self.sid); + + // Build the cred update session. + self.create_credupdate_session(sessionid, None, account, ct) + }) + } + + pub fn prune_sessions() { + todo!(); + } + + pub fn commit_credential_update( + &mut self, + cust: CredentialUpdateSessionToken, + ct: Duration, + ) -> Result<(), OperationError> { + let session_token: CredentialUpdateSessionTokenInner = self + .token_enc_key + .decrypt(&cust.token_enc) + .map_err(|e| { + admin_error!(?e, "Failed to decrypt credential update session request"); + OperationError::SessionExpired + }) + .and_then(|data| { + serde_json::from_slice(&data).map_err(|e| { + admin_error!(err = ?e, "Failed to deserialise credential update session request"); + OperationError::SerdeJsonError + }) + })?; + + if ct >= session_token.max_ttl { + trace!(?ct, ?session_token.max_ttl); + security_info!(%session_token.sessionid, "session expired"); + return Err(OperationError::SessionExpired); + } + + let session_handle = self.cred_update_sessions.remove(&session_token.sessionid) + .ok_or_else(|| { + admin_error!("No such sessionid exists on this server - may be due to a load balancer failover or replay?"); + OperationError::InvalidState + })?; + + let session = session_handle.try_lock().map_err(|_| { + admin_error!("Session already locked, unable to proceed."); + OperationError::InvalidState + })?; + + trace!(?session); + + let mut modlist = ModifyList::new(); + + // Setup mods for the various bits. We always assert an *exact* state. + + match &session.primary { + Some(ncred) => { + modlist.push_mod(Modify::Purged(AttrString::from("primary_credential"))); + let vcred = Value::new_credential("primary", ncred.clone()); + modlist.push_mod(Modify::Present( + AttrString::from("primary_credential"), + vcred, + )); + } + None => { + modlist.push_mod(Modify::Purged(AttrString::from("primary_credential"))); + } + }; + + // If an intent token was used, remove it's former value, and add it as consumed. + if let Some(intent_token_id) = session.intent_token_id { + modlist.push_mod(Modify::Removed( + AttrString::from("credential_update_intent_token"), + PartialValue::IntentToken(intent_token_id), + )); + modlist.push_mod(Modify::Present( + AttrString::from("credential_update_intent_token"), + Value::IntentToken(intent_token_id, IntentTokenState::Consumed), + )); + }; + + // Are any other checks needed? + + // Apply to the account! + trace!(?modlist, "processing change"); + + self.qs_write + .internal_modify( + // Filter as executed + &filter!(f_eq("uuid", PartialValue::new_uuidr(&session.account.uuid))), + &modlist, + ) + .map_err(|e| { + request_error!(error = ?e); + e + }) + } +} + +impl<'a> IdmServerCredUpdateTransaction<'a> { + fn get_current_session( + &self, + cust: &CredentialUpdateSessionToken, + ct: Duration, + ) -> Result { + let session_token: CredentialUpdateSessionTokenInner = self + .token_enc_key + .decrypt(&cust.token_enc) + .map_err(|e| { + admin_error!(?e, "Failed to decrypt credential update session request"); + OperationError::SessionExpired + }) + .and_then(|data| { + serde_json::from_slice(&data).map_err(|e| { + admin_error!(err = ?e, "Failed to deserialise credential update session request"); + OperationError::SerdeJsonError + }) + })?; + + // Check the TTL + if ct >= session_token.max_ttl { + trace!(?ct, ?session_token.max_ttl); + security_info!(%session_token.sessionid, "session expired"); + return Err(OperationError::SessionExpired); + } + + self.cred_update_sessions.get(&session_token.sessionid) + .ok_or_else(|| { + admin_error!("No such sessionid exists on this server - may be due to a load balancer failover or token replay?"); + OperationError::InvalidState + }) + .cloned() + } + + pub async fn credential_status( + &self, + cust: &CredentialUpdateSessionToken, + ct: Duration, + ) -> Result { + let session_handle = self.get_current_session(cust, ct)?; + let session = session_handle.lock().await; + trace!(?session); + + let status: CredentialUpdateSessionStatus = session.deref().into(); + Ok(status) + } + + fn check_password_quality( + &self, + cleartext: &str, + related_inputs: &[&str], + ) -> Result<(), PasswordQuality> { + // password strength and badlisting is always global, rather than per-pw-policy. + // pw-policy as check on the account is about requirements for mfa for example. + // + + // is the password at least 10 char? + if cleartext.len() < PW_MIN_LENGTH { + return Err(PasswordQuality::TooShort(PW_MIN_LENGTH)); + } + + // does the password pass zxcvbn? + + let entropy = zxcvbn::zxcvbn(cleartext, related_inputs).map_err(|e| { + admin_error!("zxcvbn check failure (password empty?) {:?}", e); + PasswordQuality::TooShort(PW_MIN_LENGTH) + })?; + + // check account pwpolicy (for 3 or 4)? Do we need pw strength beyond this + // or should we be enforcing mfa instead + if entropy.score() < 3 { + // The password is too week as per: + // https://docs.rs/zxcvbn/2.0.0/zxcvbn/struct.Entropy.html + let feedback: zxcvbn::feedback::Feedback = entropy + .feedback() + .as_ref() + .ok_or(OperationError::InvalidState) + .map(|v| v.clone()) + .map_err(|e| { + security_info!("zxcvbn returned no feedback when score < 3 -> {:?}", e); + PasswordQuality::TooShort(PW_MIN_LENGTH) + })?; + + security_info!(?feedback, "pw quality feedback"); + + let feedback: Vec<_> = feedback + .suggestions() + .iter() + .map(|s| { + match s { + zxcvbn::feedback::Suggestion::UseAFewWordsAvoidCommonPhrases => { + PasswordFeedback::UseAFewWordsAvoidCommonPhrases + } + zxcvbn::feedback::Suggestion::NoNeedForSymbolsDigitsOrUppercaseLetters => { + PasswordFeedback::NoNeedForSymbolsDigitsOrUppercaseLetters + } + zxcvbn::feedback::Suggestion::AddAnotherWordOrTwo => { + PasswordFeedback::AddAnotherWordOrTwo + } + zxcvbn::feedback::Suggestion::CapitalizationDoesntHelpVeryMuch => { + PasswordFeedback::CapitalizationDoesntHelpVeryMuch + } + zxcvbn::feedback::Suggestion::AllUppercaseIsAlmostAsEasyToGuessAsAllLowercase => { + PasswordFeedback::AllUppercaseIsAlmostAsEasyToGuessAsAllLowercase + } + zxcvbn::feedback::Suggestion::ReversedWordsArentMuchHarderToGuess => { + PasswordFeedback::ReversedWordsArentMuchHarderToGuess + } + zxcvbn::feedback::Suggestion::PredictableSubstitutionsDontHelpVeryMuch => { + PasswordFeedback::PredictableSubstitutionsDontHelpVeryMuch + } + zxcvbn::feedback::Suggestion::UseALongerKeyboardPatternWithMoreTurns => { + PasswordFeedback::UseALongerKeyboardPatternWithMoreTurns + } + zxcvbn::feedback::Suggestion::AvoidRepeatedWordsAndCharacters => { + PasswordFeedback::AvoidRepeatedWordsAndCharacters + } + zxcvbn::feedback::Suggestion::AvoidSequences => { + PasswordFeedback::AvoidSequences + } + zxcvbn::feedback::Suggestion::AvoidRecentYears => { + PasswordFeedback::AvoidRecentYears + } + zxcvbn::feedback::Suggestion::AvoidYearsThatAreAssociatedWithYou => { + PasswordFeedback::AvoidYearsThatAreAssociatedWithYou + } + zxcvbn::feedback::Suggestion::AvoidDatesAndYearsThatAreAssociatedWithYou => { + PasswordFeedback::AvoidDatesAndYearsThatAreAssociatedWithYou + } + } + }) + .chain(feedback.warning().map(|w| match w { + zxcvbn::feedback::Warning::StraightRowsOfKeysAreEasyToGuess => { + PasswordFeedback::StraightRowsOfKeysAreEasyToGuess + } + zxcvbn::feedback::Warning::ShortKeyboardPatternsAreEasyToGuess => { + PasswordFeedback::ShortKeyboardPatternsAreEasyToGuess + } + zxcvbn::feedback::Warning::RepeatsLikeAaaAreEasyToGuess => { + PasswordFeedback::RepeatsLikeAaaAreEasyToGuess + } + zxcvbn::feedback::Warning::RepeatsLikeAbcAbcAreOnlySlightlyHarderToGuess => { + PasswordFeedback::RepeatsLikeAbcAbcAreOnlySlightlyHarderToGuess + } + zxcvbn::feedback::Warning::ThisIsATop10Password => { + PasswordFeedback::ThisIsATop10Password + } + zxcvbn::feedback::Warning::ThisIsATop100Password => { + PasswordFeedback::ThisIsATop100Password + } + zxcvbn::feedback::Warning::ThisIsACommonPassword => { + PasswordFeedback::ThisIsACommonPassword + } + zxcvbn::feedback::Warning::ThisIsSimilarToACommonlyUsedPassword => { + PasswordFeedback::ThisIsSimilarToACommonlyUsedPassword + } + zxcvbn::feedback::Warning::SequencesLikeAbcAreEasyToGuess => { + PasswordFeedback::SequencesLikeAbcAreEasyToGuess + } + zxcvbn::feedback::Warning::RecentYearsAreEasyToGuess => { + PasswordFeedback::RecentYearsAreEasyToGuess + } + zxcvbn::feedback::Warning::AWordByItselfIsEasyToGuess => { + PasswordFeedback::AWordByItselfIsEasyToGuess + } + zxcvbn::feedback::Warning::DatesAreOftenEasyToGuess => { + PasswordFeedback::DatesAreOftenEasyToGuess + } + zxcvbn::feedback::Warning::NamesAndSurnamesByThemselvesAreEasyToGuess => { + PasswordFeedback::NamesAndSurnamesByThemselvesAreEasyToGuess + } + zxcvbn::feedback::Warning::CommonNamesAndSurnamesAreEasyToGuess => { + PasswordFeedback::CommonNamesAndSurnamesAreEasyToGuess + } + })) + .collect(); + + return Err(PasswordQuality::Feedback(feedback)); + } + + // check a password badlist to eliminate more content + // we check the password as "lower case" to help eliminate possibilities + // also, when pw_badlist_cache is read from DB, it is read as Value (iutf8 lowercase) + if (&*self.pw_badlist_cache).contains(&cleartext.to_lowercase()) { + security_info!("Password found in badlist, rejecting"); + Err(PasswordQuality::BadListed) + } else { + Ok(()) + } + } + + pub async fn credential_primary_set_password( + &self, + cust: &CredentialUpdateSessionToken, + ct: Duration, + pw: &str, + ) -> Result { + let session_handle = self.get_current_session(cust, ct)?; + let mut session = session_handle.lock().await; + trace!(?session); + + // Check pw quality (future - acc policy applies). + self.check_password_quality(pw, session.account.related_inputs().as_slice()) + .map_err(|e| match e { + PasswordQuality::TooShort(sz) => { + OperationError::PasswordQuality(vec![PasswordFeedback::TooShort(sz)]) + } + PasswordQuality::BadListed => { + OperationError::PasswordQuality(vec![PasswordFeedback::BadListed]) + } + PasswordQuality::Feedback(feedback) => OperationError::PasswordQuality(feedback), + })?; + + let ncred = match &session.primary { + Some(primary) => { + // Is there a need to update the uuid of the cred re softlocks? + primary.set_password(self.crypto_policy, pw)? + } + None => Credential::new_password_only(self.crypto_policy, pw)?, + }; + + session.primary = Some(ncred); + Ok(session.deref().into()) + } + + pub async fn credential_primary_init_totp( + &self, + cust: &CredentialUpdateSessionToken, + ct: Duration, + ) -> Result { + let session_handle = self.get_current_session(cust, ct)?; + let mut session = session_handle.lock().await; + trace!(?session); + + // Is there something else in progress? + // Or should this just cancel it .... + if !matches!(session.mfaregstate, MfaRegState::None) { + admin_info!("Invalid TOTP state, another update is in progress"); + return Err(OperationError::InvalidState); + } + + // Generate the TOTP. + let totp_token = Totp::generate_secure(TOTP_DEFAULT_STEP); + + session.mfaregstate = MfaRegState::TotpInit(totp_token); + // Now that it's in the state, it'll be in the status when returned. + Ok(session.deref().into()) + } + + pub async fn credential_primary_check_totp( + &self, + cust: &CredentialUpdateSessionToken, + ct: Duration, + totp_chal: u32, + ) -> Result { + let session_handle = self.get_current_session(cust, ct)?; + let mut session = session_handle.lock().await; + trace!(?session); + + // Are we in a totp reg state? + match &session.mfaregstate { + MfaRegState::TotpInit(totp_token) | MfaRegState::TotpTryAgain(totp_token) => { + if totp_token.verify(totp_chal, &ct) { + // It was valid. Update the credential. + let ncred = session + .primary + .as_ref() + .map(|cred| cred.update_totp(totp_token.clone())) + .ok_or_else(|| { + admin_error!("A TOTP was added, but no primary credential stub exists"); + OperationError::InvalidState + })?; + + session.primary = Some(ncred); + + // Set the state to None. + session.mfaregstate = MfaRegState::None; + Ok(session.deref().into()) + } else { + // What if it's a broken authenticator app? Google authenticator + // and authy both force sha1 and ignore the algo we send. So lets + // check that just in case. + let token_sha1 = totp_token.clone().downgrade_to_legacy(); + + if token_sha1.verify(totp_chal, &ct) { + // Greeeaaaaaatttt it's a broken app. Let's check the user + // knows this is broken, before we proceed. + session.mfaregstate = MfaRegState::TotpInvalidSha1(token_sha1); + Ok(session.deref().into()) + } else { + // Let them check again, it's a typo. + session.mfaregstate = MfaRegState::TotpTryAgain(totp_token.clone()); + Ok(session.deref().into()) + } + } + } + _ => Err(OperationError::InvalidRequestState), + } + } + + pub async fn credential_primary_accept_sha1_totp( + &self, + cust: &CredentialUpdateSessionToken, + ct: Duration, + ) -> Result { + let session_handle = self.get_current_session(cust, ct)?; + let mut session = session_handle.lock().await; + trace!(?session); + + // Are we in a totp reg state? + match &session.mfaregstate { + MfaRegState::TotpInvalidSha1(token_sha1) => { + // They have accepted it as sha1 + let ncred = session + .primary + .as_ref() + .map(|cred| cred.update_totp(token_sha1.clone())) + .ok_or_else(|| { + admin_error!("A TOTP was added, but no primary credential stub exists"); + OperationError::InvalidState + })?; + + session.primary = Some(ncred); + + // Set the state to None. + session.mfaregstate = MfaRegState::None; + Ok(session.deref().into()) + } + _ => Err(OperationError::InvalidRequestState), + } + } + + pub async fn credential_primary_remove_totp( + &self, + cust: &CredentialUpdateSessionToken, + ct: Duration, + ) -> Result { + let session_handle = self.get_current_session(cust, ct)?; + let mut session = session_handle.lock().await; + trace!(?session); + + if !matches!(session.mfaregstate, MfaRegState::None) { + admin_info!("Invalid TOTP state, another update is in progress"); + return Err(OperationError::InvalidState); + } + + let ncred = session + .primary + .as_ref() + .map(|cred| cred.remove_totp()) + .ok_or_else(|| { + admin_error!("Try to remove TOTP, but no primary credential stub exists"); + OperationError::InvalidState + })?; + + session.primary = Some(ncred); + + // Set the state to None. + session.mfaregstate = MfaRegState::None; + Ok(session.deref().into()) + } + + pub async fn credential_primary_init_backup_codes( + &self, + cust: &CredentialUpdateSessionToken, + ct: Duration, + ) -> Result { + let session_handle = self.get_current_session(cust, ct)?; + let mut session = session_handle.lock().await; + trace!(?session); + + // I think we override/map the status to inject the codes as a once-off state message. + + let codes = backup_code_from_random(); + + let ncred = session + .primary + .as_ref() + .ok_or_else(|| { + admin_error!("Tried to add backup codes, but no primary credential stub exists"); + OperationError::InvalidState + }) + .and_then(|cred| + cred.update_backup_code(BackupCodes::new(codes.clone())) + .map_err(|_| { + admin_error!("Tried to add backup codes, but MFA is not enabled on this credential yet"); + OperationError::InvalidState + }) + ) + ?; + + session.primary = Some(ncred); + + Ok(session.deref().into()).map(|mut status: CredentialUpdateSessionStatus| { + status.mfaregstate = MfaRegStateStatus::BackupCodes(codes); + status + }) + } + + pub async fn credential_primary_remove_backup_codes( + &self, + cust: &CredentialUpdateSessionToken, + ct: Duration, + ) -> Result { + let session_handle = self.get_current_session(cust, ct)?; + let mut session = session_handle.lock().await; + trace!(?session); + + let ncred = session + .primary + .as_ref() + .ok_or_else(|| { + admin_error!("Tried to add backup codes, but no primary credential stub exists"); + OperationError::InvalidState + }) + .and_then(|cred| + cred.remove_backup_code() + .map_err(|_| { + admin_error!("Tried to remove backup codes, but MFA is not enabled on this credential yet"); + OperationError::InvalidState + }) + ) + ?; + + session.primary = Some(ncred); + + Ok(session.deref().into()) + } + + pub async fn credential_primary_delete( + &self, + cust: &CredentialUpdateSessionToken, + ct: Duration, + ) -> Result { + let session_handle = self.get_current_session(cust, ct)?; + let mut session = session_handle.lock().await; + trace!(?session); + session.primary = None; + Ok(session.deref().into()) + } + + // Generate password? +} + +#[cfg(test)] +mod tests { + use super::{ + CredentialUpdateSessionToken, InitCredentialUpdateEvent, InitCredentialUpdateIntentEvent, + MfaRegStateStatus, MAXIMUM_INTENT_TTL, MINIMUM_INTENT_TTL, + }; + use crate::credential::totp::Totp; + use crate::event::{AuthEvent, AuthResult, CreateEvent}; + use crate::idm::server::IdmServer; + use crate::prelude::*; + use std::time::Duration; + + use crate::idm::AuthState; + use compiled_uuid::uuid; + use kanidm_proto::v1::{AuthMech, CredentialDetailType}; + + use async_std::task; + + const TEST_CURRENT_TIME: u64 = 6000; + const TESTPERSON_UUID: Uuid = uuid!("cf231fea-1a8f-4410-a520-fd9b1a379c86"); + + #[test] + fn test_idm_credential_update_session_init() { + run_idm_test!(|_qs: &QueryServer, + idms: &IdmServer, + _idms_delayed: &mut IdmServerDelayed| { + let ct = Duration::from_secs(TEST_CURRENT_TIME); + let mut idms_prox_write = idms.proxy_write(ct); + + let testaccount_uuid = Uuid::new_v4(); + + let e1 = entry_init!( + ("class", Value::new_class("object")), + ("class", Value::new_class("account")), + ("name", Value::new_iname("user_account_only")), + ("uuid", Value::new_uuid(testaccount_uuid)), + ("description", Value::new_utf8s("testaccount")), + ("displayname", Value::new_utf8s("testaccount")) + ); + + let e2 = entry_init!( + ("class", Value::new_class("object")), + ("class", Value::new_class("account")), + ("class", Value::new_class("person")), + ("name", Value::new_iname("testperson")), + ("uuid", Value::new_uuid(TESTPERSON_UUID)), + ("description", Value::new_utf8s("testperson")), + ("displayname", Value::new_utf8s("testperson")) + ); + + let ce = CreateEvent::new_internal(vec![e1, e2]); + let cr = idms_prox_write.qs_write.create(&ce); + assert!(cr.is_ok()); + + let testaccount = idms_prox_write + .qs_write + .internal_search_uuid(&testaccount_uuid) + .expect("failed"); + + let testperson = idms_prox_write + .qs_write + .internal_search_uuid(&TESTPERSON_UUID) + .expect("failed"); + + let idm_admin = idms_prox_write + .qs_write + .internal_search_uuid(&UUID_IDM_ADMIN) + .expect("failed"); + + // user without permission - fail + // - accounts don't have self-write permission. + + let cur = idms_prox_write.init_credential_update( + &InitCredentialUpdateEvent::new_impersonate_entry(testaccount), + ct, + ); + + assert!(matches!(cur, Err(OperationError::NotAuthorised))); + + // user with permission - success + + let cur = idms_prox_write.init_credential_update( + &InitCredentialUpdateEvent::new_impersonate_entry(testperson), + ct, + ); + + assert!(cur.is_ok()); + + // create intent token without permission - fail + + // create intent token with permission - success + + let cur = idms_prox_write.init_credential_update_intent( + &InitCredentialUpdateIntentEvent::new_impersonate_entry( + idm_admin, + TESTPERSON_UUID, + MINIMUM_INTENT_TTL, + ), + ct, + ); + + assert!(cur.is_ok()); + let intent_tok = cur.expect("Failed to create intent token!"); + + // exchange intent token - invalid - fail + // Expired + let cur = idms_prox_write + .exchange_intent_credential_update(intent_tok.clone(), ct + MINIMUM_INTENT_TTL); + + assert!(matches!(cur, Err(OperationError::SessionExpired))); + + let cur = idms_prox_write + .exchange_intent_credential_update(intent_tok.clone(), ct + MAXIMUM_INTENT_TTL); + + assert!(matches!(cur, Err(OperationError::SessionExpired))); + + // exchange intent token - success + let cur = idms_prox_write.exchange_intent_credential_update(intent_tok.clone(), ct); + + assert!(cur.is_ok()); + + // Already used. + let cur = idms_prox_write.exchange_intent_credential_update(intent_tok, ct); + + assert!(cur.is_err()); + }) + } + + fn setup_test_session(idms: &IdmServer, ct: Duration) -> CredentialUpdateSessionToken { + let mut idms_prox_write = idms.proxy_write(ct); + + let e2 = entry_init!( + ("class", Value::new_class("object")), + ("class", Value::new_class("account")), + ("class", Value::new_class("person")), + ("name", Value::new_iname("testperson")), + ("uuid", Value::new_uuid(TESTPERSON_UUID)), + ("description", Value::new_utf8s("testperson")), + ("displayname", Value::new_utf8s("testperson")) + ); + + let ce = CreateEvent::new_internal(vec![e2]); + let cr = idms_prox_write.qs_write.create(&ce); + assert!(cr.is_ok()); + + let testperson = idms_prox_write + .qs_write + .internal_search_uuid(&TESTPERSON_UUID) + .expect("failed"); + + let cur = idms_prox_write.init_credential_update( + &InitCredentialUpdateEvent::new_impersonate_entry(testperson), + ct, + ); + + idms_prox_write.commit().expect("Failed to commit txn"); + + cur.expect("Failed to start update") + } + + fn renew_test_session(idms: &IdmServer, ct: Duration) -> CredentialUpdateSessionToken { + let mut idms_prox_write = idms.proxy_write(ct); + + let testperson = idms_prox_write + .qs_write + .internal_search_uuid(&TESTPERSON_UUID) + .expect("failed"); + + let cur = idms_prox_write.init_credential_update( + &InitCredentialUpdateEvent::new_impersonate_entry(testperson), + ct, + ); + + idms_prox_write.commit().expect("Failed to commit txn"); + + cur.expect("Failed to start update") + } + + fn commit_session(idms: &IdmServer, ct: Duration, cust: CredentialUpdateSessionToken) { + let mut idms_prox_write = idms.proxy_write(ct); + + idms_prox_write + .commit_credential_update(cust, ct) + .expect("Failed to commit credential update."); + + idms_prox_write.commit().expect("Failed to commit txn"); + } + + fn check_testperson_password(idms: &IdmServer, pw: &str, ct: Duration) -> Option { + let mut idms_auth = idms.auth(); + + let auth_init = AuthEvent::named_init("testperson"); + + let r1 = task::block_on(idms_auth.auth(&auth_init, ct)); + let ar = r1.unwrap(); + let AuthResult { + sessionid, + state, + delay: _, + } = ar; + + if !matches!(state, AuthState::Choose(_)) { + debug!("Can't proceed - {:?}", state); + return None; + }; + + let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::Password); + + let r2 = task::block_on(idms_auth.auth(&auth_begin, ct)); + let ar = r2.unwrap(); + let AuthResult { + sessionid, + state, + delay: _, + } = ar; + + assert!(matches!(state, AuthState::Continue(_))); + + let pw_step = AuthEvent::cred_step_password(sessionid, pw); + + // Expect success + let r2 = task::block_on(idms_auth.auth(&pw_step, ct)); + debug!("r2 ==> {:?}", r2); + idms_auth.commit().expect("Must not fail"); + + match r2 { + Ok(AuthResult { + sessionid: _, + state: AuthState::Success(token), + delay: _, + }) => Some(token), + _ => None, + } + } + + fn check_testperson_password_totp( + idms: &IdmServer, + pw: &str, + token: &Totp, + ct: Duration, + ) -> Option { + let mut idms_auth = idms.auth(); + + let auth_init = AuthEvent::named_init("testperson"); + + let r1 = task::block_on(idms_auth.auth(&auth_init, ct)); + let ar = r1.unwrap(); + let AuthResult { + sessionid, + state, + delay: _, + } = ar; + + if !matches!(state, AuthState::Choose(_)) { + debug!("Can't proceed - {:?}", state); + return None; + }; + + let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::PasswordMfa); + + let r2 = task::block_on(idms_auth.auth(&auth_begin, ct)); + let ar = r2.unwrap(); + let AuthResult { + sessionid, + state, + delay: _, + } = ar; + + assert!(matches!(state, AuthState::Continue(_))); + + let totp = token + .do_totp_duration_from_epoch(&ct) + .expect("Failed to perform totp step"); + + let totp_step = AuthEvent::cred_step_totp(sessionid, totp); + let r2 = task::block_on(idms_auth.auth(&totp_step, ct)); + let ar = r2.unwrap(); + let AuthResult { + sessionid, + state, + delay: _, + } = ar; + + assert!(matches!(state, AuthState::Continue(_))); + + let pw_step = AuthEvent::cred_step_password(sessionid, pw); + + // Expect success + let r3 = task::block_on(idms_auth.auth(&pw_step, ct)); + debug!("r3 ==> {:?}", r3); + idms_auth.commit().expect("Must not fail"); + + match r3 { + Ok(AuthResult { + sessionid: _, + state: AuthState::Success(token), + delay: _, + }) => Some(token), + _ => None, + } + } + + fn check_testperson_password_backup_code( + idms: &IdmServer, + pw: &str, + code: &str, + ct: Duration, + ) -> Option { + let mut idms_auth = idms.auth(); + + let auth_init = AuthEvent::named_init("testperson"); + + let r1 = task::block_on(idms_auth.auth(&auth_init, ct)); + let ar = r1.unwrap(); + let AuthResult { + sessionid, + state, + delay: _, + } = ar; + + if !matches!(state, AuthState::Choose(_)) { + debug!("Can't proceed - {:?}", state); + return None; + }; + + let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::PasswordMfa); + + let r2 = task::block_on(idms_auth.auth(&auth_begin, ct)); + let ar = r2.unwrap(); + let AuthResult { + sessionid, + state, + delay: _, + } = ar; + + assert!(matches!(state, AuthState::Continue(_))); + + let code_step = AuthEvent::cred_step_backup_code(sessionid, code); + let r2 = task::block_on(idms_auth.auth(&code_step, ct)); + let ar = r2.unwrap(); + let AuthResult { + sessionid, + state, + delay: _, + } = ar; + + assert!(matches!(state, AuthState::Continue(_))); + + let pw_step = AuthEvent::cred_step_password(sessionid, pw); + + // Expect success + let r3 = task::block_on(idms_auth.auth(&pw_step, ct)); + debug!("r3 ==> {:?}", r3); + idms_auth.commit().expect("Must not fail"); + + match r3 { + Ok(AuthResult { + sessionid: _, + state: AuthState::Success(token), + delay: _, + }) => Some(token), + _ => None, + } + } + + #[test] + fn test_idm_credential_update_onboarding_create_new_pw() { + run_idm_test!(|_qs: &QueryServer, + idms: &IdmServer, + _idms_delayed: &mut IdmServerDelayed| { + let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki"; + let ct = Duration::from_secs(TEST_CURRENT_TIME); + + let cust = setup_test_session(idms, ct); + + let cutxn = idms.cred_update_transaction(); + + // Get the credential status - this should tell + // us the details of the credentials, as well as + // if they are ready and valid to commit? + let c_status = task::block_on(cutxn.credential_status(&cust, ct)) + .expect("Failed to get the current session status."); + + trace!(?c_status); + + assert!(c_status.primary.is_none()); + + // Test initially creating a credential. + // - pw first + let c_status = + task::block_on(cutxn.credential_primary_set_password(&cust, ct, test_pw)) + .expect("Failed to update the primary cred password"); + + assert!(c_status.can_commit); + + drop(cutxn); + commit_session(idms, ct, cust); + + // Check it works! + assert!(check_testperson_password(idms, test_pw, ct).is_some()); + + // Test deleting the pw + let cust = renew_test_session(idms, ct); + let cutxn = idms.cred_update_transaction(); + + let c_status = task::block_on(cutxn.credential_status(&cust, ct)) + .expect("Failed to get the current session status."); + trace!(?c_status); + assert!(c_status.primary.is_some()); + + let c_status = task::block_on(cutxn.credential_primary_delete(&cust, ct)) + .expect("Failed to delete the primary cred"); + trace!(?c_status); + assert!(c_status.primary.is_none()); + + drop(cutxn); + commit_session(idms, ct, cust); + + // Must fail now! + assert!(check_testperson_password(idms, test_pw, ct).is_none()); + }) + } + + // Test set of primary account password + // - fail pw quality checks etc + // - set correctly. + + // - setup TOTP + #[test] + fn test_idm_credential_update_onboarding_create_new_mfa_totp_basic() { + run_idm_test!(|_qs: &QueryServer, + idms: &IdmServer, + _idms_delayed: &mut IdmServerDelayed| { + let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki"; + let ct = Duration::from_secs(TEST_CURRENT_TIME); + + let cust = setup_test_session(idms, ct); + let cutxn = idms.cred_update_transaction(); + + // Setup the PW + let c_status = + task::block_on(cutxn.credential_primary_set_password(&cust, ct, test_pw)) + .expect("Failed to update the primary cred password"); + + // Since it's pw only. + assert!(c_status.can_commit); + + // + let c_status = task::block_on(cutxn.credential_primary_init_totp(&cust, ct)) + .expect("Failed to update the primary cred password"); + + // Check the status has the token. + let totp_token: Totp = match c_status.mfaregstate { + MfaRegStateStatus::TotpCheck(secret) => Some(secret.into()), + + _ => None, + } + .expect("Unable to retrieve totp token, invalid state."); + + trace!(?totp_token); + let chal = totp_token + .do_totp_duration_from_epoch(&ct) + .expect("Failed to perform totp step"); + + // Intentionally get it wrong. + let c_status = task::block_on(cutxn.credential_primary_check_totp(&cust, ct, chal + 1)) + .expect("Failed to update the primary cred password"); + + assert!(matches!( + c_status.mfaregstate, + MfaRegStateStatus::TotpTryAgain + )); + + let c_status = task::block_on(cutxn.credential_primary_check_totp(&cust, ct, chal)) + .expect("Failed to update the primary cred password"); + + assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None)); + assert!(matches!( + c_status.primary.as_ref().map(|c| &c.type_), + Some(CredentialDetailType::PasswordMfa(true, _, 0)) + )); + + // Should be okay now! + + drop(cutxn); + commit_session(idms, ct, cust); + + // Check it works! + assert!(check_testperson_password_totp(idms, test_pw, &totp_token, ct).is_some()); + // No need to test delete of the whole cred, we already did with pw above. + + // If we remove TOTP, show it reverts back. + let cust = renew_test_session(idms, ct); + let cutxn = idms.cred_update_transaction(); + + let c_status = task::block_on(cutxn.credential_primary_remove_totp(&cust, ct)) + .expect("Failed to update the primary cred password"); + + assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None)); + assert!(matches!( + c_status.primary.as_ref().map(|c| &c.type_), + Some(CredentialDetailType::Password) + )); + + drop(cutxn); + commit_session(idms, ct, cust); + + // Check it works with totp removed. + assert!(check_testperson_password(idms, test_pw, ct).is_some()); + }) + } + + // Check sha1 totp. + #[test] + fn test_idm_credential_update_onboarding_create_new_mfa_totp_sha1() { + run_idm_test!(|_qs: &QueryServer, + idms: &IdmServer, + _idms_delayed: &mut IdmServerDelayed| { + let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki"; + let ct = Duration::from_secs(TEST_CURRENT_TIME); + + let cust = setup_test_session(idms, ct); + let cutxn = idms.cred_update_transaction(); + + // Setup the PW + let c_status = + task::block_on(cutxn.credential_primary_set_password(&cust, ct, test_pw)) + .expect("Failed to update the primary cred password"); + + // Since it's pw only. + assert!(c_status.can_commit); + + // + let c_status = task::block_on(cutxn.credential_primary_init_totp(&cust, ct)) + .expect("Failed to update the primary cred password"); + + // Check the status has the token. + let totp_token: Totp = match c_status.mfaregstate { + MfaRegStateStatus::TotpCheck(secret) => Some(secret.into()), + + _ => None, + } + .expect("Unable to retrieve totp token, invalid state."); + + let totp_token = totp_token.downgrade_to_legacy(); + + trace!(?totp_token); + let chal = totp_token + .do_totp_duration_from_epoch(&ct) + .expect("Failed to perform totp step"); + + // Should getn the warn that it's sha1 + let c_status = task::block_on(cutxn.credential_primary_check_totp(&cust, ct, chal)) + .expect("Failed to update the primary cred password"); + + assert!(matches!( + c_status.mfaregstate, + MfaRegStateStatus::TotpInvalidSha1 + )); + + // Accept it + let c_status = task::block_on(cutxn.credential_primary_accept_sha1_totp(&cust, ct)) + .expect("Failed to update the primary cred password"); + + assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None)); + assert!(matches!( + c_status.primary.as_ref().map(|c| &c.type_), + Some(CredentialDetailType::PasswordMfa(true, _, 0)) + )); + + // Should be okay now! + + drop(cutxn); + commit_session(idms, ct, cust); + + // Check it works! + assert!(check_testperson_password_totp(idms, test_pw, &totp_token, ct).is_some()); + // No need to test delete, we already did with pw above. + }) + } + + #[test] + fn test_idm_credential_update_onboarding_create_new_mfa_totp_backup_codes() { + run_idm_test!( + |_qs: &QueryServer, idms: &IdmServer, idms_delayed: &mut IdmServerDelayed| { + let test_pw = + "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki"; + let ct = Duration::from_secs(TEST_CURRENT_TIME); + + let cust = setup_test_session(idms, ct); + let cutxn = idms.cred_update_transaction(); + + // Setup the PW + let _c_status = + task::block_on(cutxn.credential_primary_set_password(&cust, ct, test_pw)) + .expect("Failed to update the primary cred password"); + + // Backup codes are refused to be added because we don't have mfa yet. + assert!(matches!( + task::block_on(cutxn.credential_primary_init_backup_codes(&cust, ct)), + Err(OperationError::InvalidState) + )); + + let c_status = task::block_on(cutxn.credential_primary_init_totp(&cust, ct)) + .expect("Failed to update the primary cred password"); + + let totp_token: Totp = match c_status.mfaregstate { + MfaRegStateStatus::TotpCheck(secret) => Some(secret.into()), + + _ => None, + } + .expect("Unable to retrieve totp token, invalid state."); + + trace!(?totp_token); + let chal = totp_token + .do_totp_duration_from_epoch(&ct) + .expect("Failed to perform totp step"); + + let c_status = task::block_on(cutxn.credential_primary_check_totp(&cust, ct, chal)) + .expect("Failed to update the primary cred password"); + + assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None)); + assert!(matches!( + c_status.primary.as_ref().map(|c| &c.type_), + Some(CredentialDetailType::PasswordMfa(true, _, 0)) + )); + + // Now good to go, we need to now add our backup codes. + // Whats the right way to get these back? + let c_status = + task::block_on(cutxn.credential_primary_init_backup_codes(&cust, ct)) + .expect("Failed to update the primary cred password"); + + let codes = match c_status.mfaregstate { + MfaRegStateStatus::BackupCodes(codes) => Some(codes), + _ => None, + } + .expect("Unable to retrieve backupcodes, invalid state."); + + // Should error because the number is not 0 + debug!("{:?}", c_status.primary.as_ref().map(|c| &c.type_)); + assert!(matches!( + c_status.primary.as_ref().map(|c| &c.type_), + Some(CredentialDetailType::PasswordMfa(true, _, 8)) + )); + + // Should be okay now! + drop(cutxn); + commit_session(idms, ct, cust); + + let backup_code = codes.iter().next().expect("No codes available"); + + // Check it works! + assert!( + check_testperson_password_backup_code(idms, test_pw, backup_code, ct).is_some() + ); + + // There now should be a backup code invalidation present + let da = idms_delayed.try_recv().expect("invalid"); + let r = task::block_on(idms.delayed_action(ct, da)); + assert!(r.is_ok()); + + // Renew to start the next steps + let cust = renew_test_session(idms, ct); + let cutxn = idms.cred_update_transaction(); + + // Only 7 codes left. + let c_status = task::block_on(cutxn.credential_status(&cust, ct)) + .expect("Failed to get the current session status."); + + assert!(matches!( + c_status.primary.as_ref().map(|c| &c.type_), + Some(CredentialDetailType::PasswordMfa(true, _, 7)) + )); + + // If we remove codes, it leaves totp. + let c_status = + task::block_on(cutxn.credential_primary_remove_backup_codes(&cust, ct)) + .expect("Failed to update the primary cred password"); + + assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None)); + assert!(matches!( + c_status.primary.as_ref().map(|c| &c.type_), + Some(CredentialDetailType::PasswordMfa(true, _, 0)) + )); + + // Re-add the codes. + let c_status = + task::block_on(cutxn.credential_primary_init_backup_codes(&cust, ct)) + .expect("Failed to update the primary cred password"); + + assert!(matches!( + c_status.mfaregstate, + MfaRegStateStatus::BackupCodes(_) + )); + assert!(matches!( + c_status.primary.as_ref().map(|c| &c.type_), + Some(CredentialDetailType::PasswordMfa(true, _, 8)) + )); + + // If we remove totp, it removes codes. + let c_status = task::block_on(cutxn.credential_primary_remove_totp(&cust, ct)) + .expect("Failed to update the primary cred password"); + + assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None)); + assert!(matches!( + c_status.primary.as_ref().map(|c| &c.type_), + Some(CredentialDetailType::Password) + )); + + drop(cutxn); + commit_session(idms, ct, cust); + } + ) + } + + // Primary cred must be pw or pwmfa + + // - setup webauthn + // - remove webauthn + // - test mulitple webauthn token. + + // W_ policy, assert can't remove MFA if it's enforced. + + // enroll trusted device + // remove trusted device. + // trusted device flag changes? + + // Any policy checks we care about? + + // Others in the future +} diff --git a/kanidmd/src/lib/idm/delayed.rs b/kanidmd/idm/src/idm/delayed.rs similarity index 100% rename from kanidmd/src/lib/idm/delayed.rs rename to kanidmd/idm/src/idm/delayed.rs diff --git a/kanidmd/src/lib/idm/event.rs b/kanidmd/idm/src/idm/event.rs similarity index 99% rename from kanidmd/src/lib/idm/event.rs rename to kanidmd/idm/src/idm/event.rs index e79e5c9ab..5f83092a9 100644 --- a/kanidmd/src/lib/idm/event.rs +++ b/kanidmd/idm/src/idm/event.rs @@ -1,7 +1,5 @@ use crate::prelude::*; -use uuid::Uuid; - use kanidm_proto::v1::OperationError; use webauthn_rs::proto::RegisterPublicKeyCredential; @@ -25,11 +23,11 @@ impl PasswordChangeEvent { cleartext: String, // qs: &QueryServerWriteTransaction, ) -> Result { - let u = ident.get_uuid().ok_or(OperationError::InvalidState)?; + let target = ident.get_uuid().ok_or(OperationError::InvalidState)?; Ok(PasswordChangeEvent { ident, - target: u, + target, cleartext, }) } diff --git a/kanidmd/src/lib/idm/group.rs b/kanidmd/idm/src/idm/group.rs similarity index 100% rename from kanidmd/src/lib/idm/group.rs rename to kanidmd/idm/src/idm/group.rs diff --git a/kanidmd/src/lib/idm/mfareg.rs b/kanidmd/idm/src/idm/mfareg.rs similarity index 100% rename from kanidmd/src/lib/idm/mfareg.rs rename to kanidmd/idm/src/idm/mfareg.rs diff --git a/kanidmd/src/lib/idm/mod.rs b/kanidmd/idm/src/idm/mod.rs similarity index 95% rename from kanidmd/src/lib/idm/mod.rs rename to kanidmd/idm/src/idm/mod.rs index 955d958b6..06d978c2a 100644 --- a/kanidmd/src/lib/idm/mod.rs +++ b/kanidmd/idm/src/idm/mod.rs @@ -5,6 +5,7 @@ pub(crate) mod account; pub(crate) mod authsession; +pub(crate) mod credupdatesession; pub(crate) mod delayed; pub(crate) mod event; pub(crate) mod group; diff --git a/kanidmd/src/lib/idm/oauth2.rs b/kanidmd/idm/src/idm/oauth2.rs similarity index 100% rename from kanidmd/src/lib/idm/oauth2.rs rename to kanidmd/idm/src/idm/oauth2.rs diff --git a/kanidmd/src/lib/idm/radius.rs b/kanidmd/idm/src/idm/radius.rs similarity index 100% rename from kanidmd/src/lib/idm/radius.rs rename to kanidmd/idm/src/idm/radius.rs diff --git a/kanidmd/src/lib/idm/server.rs b/kanidmd/idm/src/idm/server.rs similarity index 98% rename from kanidmd/src/lib/idm/server.rs rename to kanidmd/idm/src/idm/server.rs index cfe9f768c..31b3ea7d9 100644 --- a/kanidmd/src/lib/idm/server.rs +++ b/kanidmd/idm/src/idm/server.rs @@ -6,6 +6,7 @@ use crate::event::{AuthEvent, AuthEventStep, AuthResult}; use crate::identity::{IdentType, IdentUser, Limits}; use crate::idm::account::Account; use crate::idm::authsession::AuthSession; +use crate::idm::credupdatesession::CredentialUpdateSessionMutex; use crate::idm::event::{ AcceptSha1TotpEvent, CredentialStatusEvent, GeneratePasswordEvent, GenerateTotpEvent, LdapAuthEvent, PasswordChangeEvent, RadiusAuthTokenEvent, RegenerateRadiusSecretEvent, @@ -38,8 +39,8 @@ use crate::idm::delayed::{ use hashbrown::HashSet; use kanidm_proto::v1::{ - AuthType, BackupCodesView, CredentialStatus, RadiusAuthToken, SetCredentialResponse, - UnixGroupToken, UnixUserToken, UserAuthToken, + AuthType, BackupCodesView, CredentialStatus, PasswordFeedback, RadiusAuthToken, + SetCredentialResponse, UnixGroupToken, UnixUserToken, UserAuthToken, }; use compact_jwt::{Jws, JwsSigner, JwsUnverified, JwsValidator}; @@ -56,7 +57,7 @@ use core::task::{Context, Poll}; use futures::task as futures_task; use concread::{ - bptree::{BptreeMap, BptreeMapWriteTxn}, + bptree::{BptreeMap, BptreeMapReadTxn, BptreeMapWriteTxn}, cowcell::{CowCellReadTxn, CowCellWriteTxn}, hashmap::HashMap, CowCell, @@ -79,8 +80,6 @@ use tracing::trace; type AuthSessionMutex = Arc>; type CredSoftLockMutex = Arc>; -// type CredUpdateSessionMutex = Arc>; - pub struct IdmServer { // There is a good reason to keep this single thread - it // means that limits to sessions can be easily applied and checked to @@ -91,7 +90,7 @@ pub struct IdmServer { softlocks: HashMap, /// A set of in progress MFA registrations mfareg_sessions: BptreeMap, - // cred_update_sessions: BptreeMap, + cred_update_sessions: BptreeMap, /// Reference to the query server. qs: QueryServer, /// The configured crypto policy for the IDM server. Later this could be transactional and loaded from the db similar to access. But today it's just to allow dynamic pbkdf2rounds @@ -124,6 +123,16 @@ pub struct IdmServerAuthTransaction<'a> { uat_jwt_validator: CowCellReadTxn, } +pub(crate) struct IdmServerCredUpdateTransaction<'a> { + pub _qs_read: QueryServerReadTransaction<'a>, + // sid: Sid, + pub _webauthn: &'a Webauthn, + pub pw_badlist_cache: CowCellReadTxn>, + pub cred_update_sessions: BptreeMapReadTxn<'a, Uuid, CredentialUpdateSessionMutex>, + pub token_enc_key: CowCellReadTxn, + pub crypto_policy: &'a CryptoPolicy, +} + /// This contains read-only methods, like getting users, groups and other structured content. pub struct IdmServerProxyReadTransaction<'a> { pub qs_read: QueryServerReadTransaction<'a>, @@ -137,13 +146,14 @@ pub struct IdmServerProxyWriteTransaction<'a> { pub qs_write: QueryServerWriteTransaction<'a>, /// Associate to an event origin ID, which has a TS and a UUID instead mfareg_sessions: BptreeMapWriteTxn<'a, Uuid, MfaRegSession>, - sid: Sid, + pub(crate) cred_update_sessions: BptreeMapWriteTxn<'a, Uuid, CredentialUpdateSessionMutex>, + pub(crate) sid: Sid, crypto_policy: &'a CryptoPolicy, webauthn: &'a Webauthn, pw_badlist_cache: CowCellWriteTxn<'a, HashSet>, uat_jwt_signer: CowCellWriteTxn<'a, JwsSigner>, uat_jwt_validator: CowCellWriteTxn<'a, JwsValidator>, - token_enc_key: CowCellWriteTxn<'a, Fernet>, + pub(crate) token_enc_key: CowCellWriteTxn<'a, Fernet>, oauth2rs: Oauth2ResourceServersWriteTransaction<'a>, } @@ -246,6 +256,7 @@ impl IdmServer { sessions: BptreeMap::new(), softlocks: HashMap::new(), mfareg_sessions: BptreeMap::new(), + cred_update_sessions: BptreeMap::new(), qs, crypto_policy, async_tx, @@ -312,6 +323,7 @@ impl IdmServer { IdmServerProxyWriteTransaction { mfareg_sessions: self.mfareg_sessions.write(), + cred_update_sessions: self.cred_update_sessions.write(), qs_write, sid, crypto_policy: &self.crypto_policy, @@ -324,6 +336,23 @@ impl IdmServer { } } + #[cfg(test)] + pub(crate) fn cred_update_transaction(&self) -> IdmServerCredUpdateTransaction<'_> { + task::block_on(self.cred_update_transaction_async()) + } + + pub(crate) async fn cred_update_transaction_async(&self) -> IdmServerCredUpdateTransaction<'_> { + IdmServerCredUpdateTransaction { + _qs_read: self.qs.read_async().await, + // sid: Sid, + _webauthn: &self.webauthn, + pw_badlist_cache: self.pw_badlist_cache.read(), + cred_update_sessions: self.cred_update_sessions.read(), + token_enc_key: self.token_enc_key.read(), + crypto_policy: &self.crypto_policy, + } + } + #[cfg(test)] pub(crate) async fn delayed_action( &self, @@ -574,6 +603,8 @@ impl<'a> IdmServerAuthTransaction<'a> { // out of the session tree. let account = Account::try_from_entry_ro(entry.as_ref(), &mut self.qs_read)?; + trace!(?account.primary); + // Intent to take both trees to write. let _session_ticket = self.session_ticket.acquire().await; @@ -1247,14 +1278,16 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { // is the password at least 10 char? if cleartext.len() < PW_MIN_LENGTH { - return Err(OperationError::PasswordTooShort(PW_MIN_LENGTH)); + return Err(OperationError::PasswordQuality(vec![ + PasswordFeedback::TooShort(PW_MIN_LENGTH), + ])); } // does the password pass zxcvbn? let entropy = zxcvbn::zxcvbn(cleartext, related_inputs).map_err(|e| { admin_error!("zxcvbn check failure (password empty?) {:?}", e); - OperationError::PasswordEmpty + OperationError::PasswordQuality(vec![PasswordFeedback::TooShort(PW_MIN_LENGTH)]) })?; // check account pwpolicy (for 3 or 4)? Do we need pw strength beyond this @@ -1275,7 +1308,10 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { security_info!(?feedback, "pw quality feedback"); // return Err(OperationError::PasswordTooWeak(feedback)) - return Err(OperationError::PasswordTooWeak); + // return Err(OperationError::PasswordTooWeak); + return Err(OperationError::PasswordQuality(vec![ + PasswordFeedback::BadListed, + ])); } // check a password badlist to eliminate more content @@ -1283,7 +1319,9 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { // also, when pw_badlist_cache is read from DB, it is read as Value (iutf8 lowercase) if (&*self.pw_badlist_cache).contains(&cleartext.to_lowercase()) { security_info!("Password found in badlist, rejecting"); - Err(OperationError::PasswordBadListed) + Err(OperationError::PasswordQuality(vec![ + PasswordFeedback::BadListed, + ])) } else { Ok(()) } @@ -2073,6 +2111,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { self.token_enc_key.commit(); self.pw_badlist_cache.commit(); self.mfareg_sessions.commit(); + self.cred_update_sessions.commit(); self.qs_write.commit() }) } @@ -3801,7 +3840,7 @@ mod tests { } #[test] - fn test_idm_bundy_uat_expiry() { + fn test_idm_jwt_uat_expiry() { run_idm_test!( |qs: &QueryServer, idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed| { let ct = Duration::from_secs(TEST_CURRENT_TIME); @@ -3931,7 +3970,7 @@ mod tests { } #[test] - fn test_idm_bundy_uat_token_key_reload() { + fn test_idm_jwt_uat_token_key_reload() { run_idm_test!( |qs: &QueryServer, idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed| { let ct = Duration::from_secs(TEST_CURRENT_TIME); diff --git a/kanidmd/src/lib/idm/unix.rs b/kanidmd/idm/src/idm/unix.rs similarity index 100% rename from kanidmd/src/lib/idm/unix.rs rename to kanidmd/idm/src/idm/unix.rs diff --git a/kanidmd/src/lib/interval.rs b/kanidmd/idm/src/interval.rs similarity index 100% rename from kanidmd/src/lib/interval.rs rename to kanidmd/idm/src/interval.rs diff --git a/kanidmd/src/lib/ldap.rs b/kanidmd/idm/src/ldap.rs similarity index 100% rename from kanidmd/src/lib/ldap.rs rename to kanidmd/idm/src/ldap.rs diff --git a/kanidmd/src/lib/lib.rs b/kanidmd/idm/src/lib.rs similarity index 97% rename from kanidmd/src/lib/lib.rs rename to kanidmd/idm/src/lib.rs index 6e1351c46..b64a7e648 100644 --- a/kanidmd/src/lib/lib.rs +++ b/kanidmd/idm/src/lib.rs @@ -2,7 +2,7 @@ //! which is used to process authentication, store identities and enforce access controls. #![recursion_limit = "512"] -#![deny(warnings)] +// #![deny(warnings)] #![warn(unused_extern_crates)] #![deny(clippy::todo)] #![deny(clippy::unimplemented)] @@ -43,8 +43,8 @@ pub mod identity; pub mod interval; pub mod ldap; mod modify; -pub(crate) mod value; -pub(crate) mod valueset; +pub mod value; +pub mod valueset; #[macro_use] mod plugins; mod access; diff --git a/kanidmd/src/lib/macros.rs b/kanidmd/idm/src/macros.rs similarity index 99% rename from kanidmd/src/lib/macros.rs rename to kanidmd/idm/src/macros.rs index 5d6aee46f..d5d4bacd2 100644 --- a/kanidmd/src/lib/macros.rs +++ b/kanidmd/idm/src/macros.rs @@ -171,8 +171,6 @@ macro_rules! run_idm_test_inner { use crate::prelude::*; #[allow(unused_imports)] use crate::schema::Schema; - - let _ = crate::tracing_tree::test_init(); /* use env_logger; ::std::env::set_var("RUST_LOG", "actix_web=debug,kanidm=debug"); @@ -213,6 +211,7 @@ where &crate::idm::server::IdmServerDelayed, ), { + let _ = crate::tracing_tree::test_level(tracing::Level::ERROR); let _ = run_idm_test_inner!(test_fn); } diff --git a/kanidmd/src/lib/modify.rs b/kanidmd/idm/src/modify.rs similarity index 100% rename from kanidmd/src/lib/modify.rs rename to kanidmd/idm/src/modify.rs diff --git a/kanidmd/src/lib/plugins/attrunique.rs b/kanidmd/idm/src/plugins/attrunique.rs similarity index 100% rename from kanidmd/src/lib/plugins/attrunique.rs rename to kanidmd/idm/src/plugins/attrunique.rs diff --git a/kanidmd/src/lib/plugins/base.rs b/kanidmd/idm/src/plugins/base.rs similarity index 100% rename from kanidmd/src/lib/plugins/base.rs rename to kanidmd/idm/src/plugins/base.rs diff --git a/kanidmd/src/lib/plugins/domain.rs b/kanidmd/idm/src/plugins/domain.rs similarity index 100% rename from kanidmd/src/lib/plugins/domain.rs rename to kanidmd/idm/src/plugins/domain.rs diff --git a/kanidmd/src/lib/plugins/failure.rs b/kanidmd/idm/src/plugins/failure.rs similarity index 100% rename from kanidmd/src/lib/plugins/failure.rs rename to kanidmd/idm/src/plugins/failure.rs diff --git a/kanidmd/src/lib/plugins/gidnumber.rs b/kanidmd/idm/src/plugins/gidnumber.rs similarity index 100% rename from kanidmd/src/lib/plugins/gidnumber.rs rename to kanidmd/idm/src/plugins/gidnumber.rs diff --git a/kanidmd/src/lib/plugins/memberof.rs b/kanidmd/idm/src/plugins/memberof.rs similarity index 100% rename from kanidmd/src/lib/plugins/memberof.rs rename to kanidmd/idm/src/plugins/memberof.rs diff --git a/kanidmd/src/lib/plugins/mod.rs b/kanidmd/idm/src/plugins/mod.rs similarity index 100% rename from kanidmd/src/lib/plugins/mod.rs rename to kanidmd/idm/src/plugins/mod.rs diff --git a/kanidmd/src/lib/plugins/oauth2.rs b/kanidmd/idm/src/plugins/oauth2.rs similarity index 100% rename from kanidmd/src/lib/plugins/oauth2.rs rename to kanidmd/idm/src/plugins/oauth2.rs diff --git a/kanidmd/src/lib/plugins/password_import.rs b/kanidmd/idm/src/plugins/password_import.rs similarity index 100% rename from kanidmd/src/lib/plugins/password_import.rs rename to kanidmd/idm/src/plugins/password_import.rs diff --git a/kanidmd/src/lib/plugins/protected.rs b/kanidmd/idm/src/plugins/protected.rs similarity index 100% rename from kanidmd/src/lib/plugins/protected.rs rename to kanidmd/idm/src/plugins/protected.rs diff --git a/kanidmd/src/lib/plugins/recycle.rs b/kanidmd/idm/src/plugins/recycle.rs similarity index 100% rename from kanidmd/src/lib/plugins/recycle.rs rename to kanidmd/idm/src/plugins/recycle.rs diff --git a/kanidmd/src/lib/plugins/refint.rs b/kanidmd/idm/src/plugins/refint.rs similarity index 100% rename from kanidmd/src/lib/plugins/refint.rs rename to kanidmd/idm/src/plugins/refint.rs diff --git a/kanidmd/src/lib/plugins/spn.rs b/kanidmd/idm/src/plugins/spn.rs similarity index 100% rename from kanidmd/src/lib/plugins/spn.rs rename to kanidmd/idm/src/plugins/spn.rs diff --git a/kanidmd/src/lib/repl/cid.rs b/kanidmd/idm/src/repl/cid.rs similarity index 100% rename from kanidmd/src/lib/repl/cid.rs rename to kanidmd/idm/src/repl/cid.rs diff --git a/kanidmd/src/lib/repl/mod.rs b/kanidmd/idm/src/repl/mod.rs similarity index 100% rename from kanidmd/src/lib/repl/mod.rs rename to kanidmd/idm/src/repl/mod.rs diff --git a/kanidmd/src/lib/schema.rs b/kanidmd/idm/src/schema.rs similarity index 99% rename from kanidmd/src/lib/schema.rs rename to kanidmd/idm/src/schema.rs index f1fc0b51d..44df323eb 100644 --- a/kanidmd/src/lib/schema.rs +++ b/kanidmd/idm/src/schema.rs @@ -196,6 +196,7 @@ impl SchemaAttribute { SyntaxType::OauthScope => v.is_oauthscope(), SyntaxType::OauthScopeMap => v.is_oauthscopemap() || v.is_refer(), SyntaxType::PrivateBinary => v.is_privatebinary(), + SyntaxType::IntentToken => matches!(v, PartialValue::IntentToken(_)), }; if r { Ok(()) @@ -235,6 +236,7 @@ impl SchemaAttribute { SyntaxType::OauthScope => v.is_oauthscope(), SyntaxType::OauthScopeMap => v.is_oauthscopemap() || v.is_refer(), SyntaxType::PrivateBinary => v.is_privatebinary(), + SyntaxType::IntentToken => matches!(v, Value::IntentToken(_, _)), }; if r { Ok(()) @@ -281,6 +283,7 @@ impl SchemaAttribute { SyntaxType::OauthScope => ava.is_oauthscope(), SyntaxType::OauthScopeMap => ava.is_oauthscopemap(), SyntaxType::PrivateBinary => ava.is_privatebinary(), + SyntaxType::IntentToken => ava.is_intenttoken(), }; if valid && ava.validate() { Ok(()) diff --git a/kanidmd/src/lib/server.rs b/kanidmd/idm/src/server.rs similarity index 99% rename from kanidmd/src/lib/server.rs rename to kanidmd/idm/src/server.rs index 8e0dd9f34..afccaee77 100644 --- a/kanidmd/src/lib/server.rs +++ b/kanidmd/idm/src/server.rs @@ -514,6 +514,7 @@ pub trait QueryServerTransaction<'a> { .ok_or_else(|| OperationError::InvalidAttribute("Invalid Oauth Scope syntax".to_string())), SyntaxType::OauthScopeMap => Err(OperationError::InvalidAttribute("Oauth Scope Maps can not be supplied through modification - please use the IDM api".to_string())), SyntaxType::PrivateBinary => Err(OperationError::InvalidAttribute("Private Binary Values can not be supplied through modification".to_string())), + SyntaxType::IntentToken => Err(OperationError::InvalidAttribute("Intent Token Values can not be supplied through modification".to_string())), } } None => { @@ -634,6 +635,13 @@ pub trait QueryServerTransaction<'a> { }), SyntaxType::OauthScope => Ok(PartialValue::new_oauthscope(value)), SyntaxType::PrivateBinary => Ok(PartialValue::PrivateBinary), + SyntaxType::IntentToken => { + PartialValue::new_intenttoken_s(value).ok_or_else(|| { + OperationError::InvalidAttribute( + "Invalid Intent Token ID (uuid) syntax".to_string(), + ) + }) + } } } None => { @@ -2295,6 +2303,7 @@ impl<'a> QueryServerWriteTransaction<'a> { JSON_SCHEMA_ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE, JSON_SCHEMA_ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE, JSON_SCHEMA_ATTR_RS256_PRIVATE_KEY_DER, + JSON_SCHEMA_ATTR_CREDENTIAL_UPDATE_INTENT_TOKEN, JSON_SCHEMA_CLASS_PERSON, JSON_SCHEMA_CLASS_ORGPERSON, JSON_SCHEMA_CLASS_GROUP, diff --git a/kanidmd/src/lib/status.rs b/kanidmd/idm/src/status.rs similarity index 100% rename from kanidmd/src/lib/status.rs rename to kanidmd/idm/src/status.rs diff --git a/kanidmd/src/lib/tracing_tree/event_tag.rs b/kanidmd/idm/src/tracing_tree/event_tag.rs similarity index 100% rename from kanidmd/src/lib/tracing_tree/event_tag.rs rename to kanidmd/idm/src/tracing_tree/event_tag.rs diff --git a/kanidmd/src/lib/tracing_tree/formatter.rs b/kanidmd/idm/src/tracing_tree/formatter.rs similarity index 100% rename from kanidmd/src/lib/tracing_tree/formatter.rs rename to kanidmd/idm/src/tracing_tree/formatter.rs diff --git a/kanidmd/src/lib/tracing_tree/macros.rs b/kanidmd/idm/src/tracing_tree/macros.rs similarity index 100% rename from kanidmd/src/lib/tracing_tree/macros.rs rename to kanidmd/idm/src/tracing_tree/macros.rs diff --git a/kanidmd/src/lib/tracing_tree/middleware.rs b/kanidmd/idm/src/tracing_tree/middleware.rs similarity index 100% rename from kanidmd/src/lib/tracing_tree/middleware.rs rename to kanidmd/idm/src/tracing_tree/middleware.rs diff --git a/kanidmd/src/lib/tracing_tree/mod.rs b/kanidmd/idm/src/tracing_tree/mod.rs similarity index 60% rename from kanidmd/src/lib/tracing_tree/mod.rs rename to kanidmd/idm/src/tracing_tree/mod.rs index 9dd40771d..679bc92fc 100644 --- a/kanidmd/src/lib/tracing_tree/mod.rs +++ b/kanidmd/idm/src/tracing_tree/mod.rs @@ -8,4 +8,6 @@ mod timings; pub use event_tag::EventTag; pub use middleware::TreeMiddleware; -pub use subscriber::{main_init, operation_id, test_init, TreePreProcessed, TreeSubscriber}; +pub use subscriber::{ + main_init, operation_id, test_init, test_level, TreePreProcessed, TreeSubscriber, +}; diff --git a/kanidmd/src/lib/tracing_tree/processor.rs b/kanidmd/idm/src/tracing_tree/processor.rs similarity index 100% rename from kanidmd/src/lib/tracing_tree/processor.rs rename to kanidmd/idm/src/tracing_tree/processor.rs diff --git a/kanidmd/src/lib/tracing_tree/subscriber.rs b/kanidmd/idm/src/tracing_tree/subscriber.rs similarity index 96% rename from kanidmd/src/lib/tracing_tree/subscriber.rs rename to kanidmd/idm/src/tracing_tree/subscriber.rs index ca74bfa0d..a674ba2e4 100644 --- a/kanidmd/src/lib/tracing_tree/subscriber.rs +++ b/kanidmd/idm/src/tracing_tree/subscriber.rs @@ -556,10 +556,27 @@ pub fn main_init() -> JoinHandle<()> { // and then getting dropped, making the global subscriber panic on further attempts to send logs. #[allow(dead_code)] pub fn test_init() -> Result<(), SetGlobalDefaultError> { - tracing::subscriber::set_global_default(TreeSubscriber { + let subscriber = TreeSubscriber { inner: Registry::default().with(TreeLayer { fmt: LogFmt::Pretty, processor: TestProcessor {}, }), - }) + }; + + tracing::subscriber::set_global_default(subscriber) +} + +#[allow(dead_code)] +pub fn test_level(level: tracing::Level) -> Result<(), SetGlobalDefaultError> { + let subscriber = TreeSubscriber { + inner: Registry::default().with(TreeLayer { + fmt: LogFmt::Pretty, + processor: TestProcessor {}, + }), + }; + + let subscriber = + tracing_subscriber::filter::LevelFilter::from_level(level).with_subscriber(subscriber); + + tracing::subscriber::set_global_default(subscriber) } diff --git a/kanidmd/src/lib/tracing_tree/timings.rs b/kanidmd/idm/src/tracing_tree/timings.rs similarity index 100% rename from kanidmd/src/lib/tracing_tree/timings.rs rename to kanidmd/idm/src/tracing_tree/timings.rs diff --git a/kanidmd/src/lib/utils.rs b/kanidmd/idm/src/utils.rs similarity index 100% rename from kanidmd/src/lib/utils.rs rename to kanidmd/idm/src/utils.rs diff --git a/kanidmd/src/lib/value.rs b/kanidmd/idm/src/value.rs similarity index 97% rename from kanidmd/src/lib/value.rs rename to kanidmd/idm/src/value.rs index 5cd46a634..e44b5d0c5 100644 --- a/kanidmd/src/lib/value.rs +++ b/kanidmd/idm/src/value.rs @@ -68,6 +68,13 @@ pub enum IndexType { SubString, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum IntentTokenState { + Valid, + InProgress(Uuid, Duration), + Consumed, +} + impl TryFrom<&str> for IndexType { type Error = (); @@ -154,6 +161,7 @@ pub enum SyntaxType { OauthScope, OauthScopeMap, PrivateBinary, + IntentToken, } impl TryFrom<&str> for SyntaxType { @@ -185,6 +193,7 @@ impl TryFrom<&str> for SyntaxType { "OAUTH_SCOPE" => Ok(SyntaxType::OauthScope), "OAUTH_SCOPE_MAP" => Ok(SyntaxType::OauthScopeMap), "PRIVATE_BINARY" => Ok(SyntaxType::PrivateBinary), + "INTENT_TOKEN" => Ok(SyntaxType::IntentToken), _ => Err(()), } } @@ -217,6 +226,7 @@ impl TryFrom for SyntaxType { 19 => Ok(SyntaxType::OauthScope), 20 => Ok(SyntaxType::OauthScopeMap), 21 => Ok(SyntaxType::PrivateBinary), + 22 => Ok(SyntaxType::IntentToken), _ => Err(()), } } @@ -247,6 +257,7 @@ impl SyntaxType { SyntaxType::OauthScope => 19, SyntaxType::OauthScopeMap => 20, SyntaxType::PrivateBinary => 21, + SyntaxType::IntentToken => 22, } } } @@ -276,6 +287,7 @@ impl fmt::Display for SyntaxType { SyntaxType::OauthScope => "OAUTH_SCOPE", SyntaxType::OauthScopeMap => "OAUTH_SCOPE_MAP", SyntaxType::PrivateBinary => "PRIVATE_BINARY", + SyntaxType::IntentToken => "INTENT_TOKEN", }) } } @@ -320,6 +332,9 @@ pub enum PartialValue { // Enumeration(String), // Float64(f64), RestrictedString(String), + IntentToken(Uuid), + TrustedDeviceEnrollment(Uuid), + AuthSession(Uuid), } impl From for PartialValue { @@ -631,6 +646,13 @@ impl PartialValue { PartialValue::RestrictedString(s.to_string()) } + pub fn new_intenttoken_s(us: &str) -> Option { + match Uuid::parse_str(us) { + Ok(u) => Some(PartialValue::IntentToken(u)), + Err(_) => None, + } + } + pub fn to_str(&self) -> Option<&str> { match self { PartialValue::Utf8(s) => Some(s.as_str()), @@ -682,6 +704,9 @@ impl PartialValue { PartialValue::OauthScopeMap(u) => u.to_hyphenated_ref().to_string(), PartialValue::Address(a) => a.to_string(), PartialValue::PhoneNumber(a) => a.to_string(), + PartialValue::IntentToken(u) => u.to_hyphenated_ref().to_string(), + PartialValue::TrustedDeviceEnrollment(u) => u.to_hyphenated_ref().to_string(), + PartialValue::AuthSession(u) => u.to_hyphenated_ref().to_string(), } } @@ -726,6 +751,9 @@ pub enum Value { // Enumeration(String), // Float64(f64), RestrictedString(String), + IntentToken(Uuid, IntentTokenState), + TrustedDeviceEnrollment(Uuid), + AuthSession(Uuid), } impl PartialEq for Value { @@ -1411,6 +1439,27 @@ impl Value { } } + pub fn to_intenttoken(self) -> Option<(Uuid, IntentTokenState)> { + match self { + Value::IntentToken(u, s) => Some((u, s)), + _ => None, + } + } + + pub fn to_trusteddeviceenrollment(self) -> Option<(Uuid, ())> { + match self { + Value::TrustedDeviceEnrollment(u) => Some((u, ())), + _ => None, + } + } + + pub fn to_authsession(self) -> Option<(Uuid, ())> { + match self { + Value::AuthSession(u) => Some((u, ())), + _ => None, + } + } + pub fn migrate_iutf8_iname(self) -> Option { match self { Value::Iutf8(v) => Some(Value::Iname(v)), diff --git a/kanidmd/src/lib/valueset.rs b/kanidmd/idm/src/valueset.rs similarity index 88% rename from kanidmd/src/lib/valueset.rs rename to kanidmd/idm/src/valueset.rs index 52d3e8d51..771436245 100644 --- a/kanidmd/src/lib/valueset.rs +++ b/kanidmd/idm/src/valueset.rs @@ -13,10 +13,10 @@ use time::OffsetDateTime; use tracing::trace; use crate::be::dbvalue::{ - DbCidV1, DbValueAddressV1, DbValueCredV1, DbValueEmailAddressV1, DbValueOauthScopeMapV1, - DbValuePhoneNumberV1, DbValueTaggedStringV1, DbValueV1, + DbCidV1, DbValueAddressV1, DbValueCredV1, DbValueEmailAddressV1, DbValueIntentTokenStateV1, + DbValueOauthScopeMapV1, DbValuePhoneNumberV1, DbValueTaggedStringV1, DbValueV1, }; -use crate::value::{Address, INAME_RE, NSUNIQUEID_RE, OAUTHSCOPE_RE}; +use crate::value::{Address, IntentTokenState, INAME_RE, NSUNIQUEID_RE, OAUTHSCOPE_RE}; #[derive(Debug, Clone)] enum I { @@ -34,6 +34,9 @@ enum I { SecretValue(SmolSet<[String; 1]>), Spn(BTreeSet<(String, String)>), Uint32(SmolSet<[u32; 1]>), + // Uint64(SmolSet<[u64; 1]>), + // Int64(SmolSet<[i64; 1]>), + // Float64(Vec<[f64; 1]>), Cid(SmolSet<[Cid; 1]>), Nsuniqueid(BTreeSet), DateTime(SmolSet<[OffsetDateTime; 1]>), @@ -53,9 +56,10 @@ enum I { OauthScopeMap(BTreeMap>), PrivateBinary(SmolSet<[Vec; 1]>), PublicBinary(BTreeMap>), - // Enumeration(SmolSet<[String; 1]>), - // Float64(Vec<[f64; 1]>), RestrictedString(BTreeSet), + IntentToken(BTreeMap), + TrustedDeviceEnrollment(BTreeMap), + AuthSession(BTreeMap), } pub struct ValueSet { @@ -126,6 +130,9 @@ impl From for ValueSet { Value::Address(a) => I::Address { set: btreeset![a] }, Value::PublicBinary(tag, bin) => I::PublicBinary(btreemap![(tag, bin)]), Value::RestrictedString(s) => I::RestrictedString(btreeset![s]), + Value::IntentToken(u, t) => I::IntentToken(btreemap![(u, t)]), + Value::TrustedDeviceEnrollment(u) => I::TrustedDeviceEnrollment(btreemap![(u, ())]), + Value::AuthSession(u) => I::AuthSession(btreemap![(u, ())]), }, } } @@ -216,6 +223,20 @@ impl TryFrom for ValueSet { // I::Address { set: btreeset![a] }, DbValueV1::PublicBinary(tag, bin) => I::PublicBinary(btreemap![(tag, bin)]), DbValueV1::RestrictedString(s) => I::RestrictedString(btreeset![s]), + DbValueV1::IntentToken { u, s } => { + let ts = match s { + DbValueIntentTokenStateV1::Valid => IntentTokenState::Valid, + DbValueIntentTokenStateV1::InProgress(pu, pd) => { + IntentTokenState::InProgress(pu, pd) + } + DbValueIntentTokenStateV1::Consumed => IntentTokenState::Consumed, + }; + I::IntentToken(btreemap![(u, ts)]) + } + DbValueV1::TrustedDeviceEnrollment { u } => { + I::TrustedDeviceEnrollment(btreemap![(u, ())]) + } + DbValueV1::AuthSession { u } => I::AuthSession(btreemap![(u, ())]), }, }) } @@ -305,6 +326,30 @@ impl ValueSet { } } (I::RestrictedString(set), Value::RestrictedString(s)) => Ok(set.insert(s)), + (I::IntentToken(map), Value::IntentToken(u, ts)) => { + if let BTreeEntry::Vacant(e) = map.entry(u) { + e.insert(ts); + Ok(true) + } else { + Ok(false) + } + } + (I::TrustedDeviceEnrollment(map), Value::TrustedDeviceEnrollment(u)) => { + if let BTreeEntry::Vacant(e) = map.entry(u) { + e.insert(()); + Ok(true) + } else { + Ok(false) + } + } + (I::AuthSession(map), Value::AuthSession(u)) => { + if let BTreeEntry::Vacant(e) = map.entry(u) { + e.insert(()); + Ok(true) + } else { + Ok(false) + } + } (_, _) => Err(OperationError::InvalidValueState), } } @@ -419,6 +464,38 @@ impl ValueSet { } } (I::RestrictedString(set), DbValueV1::RestrictedString(s)) => Ok(set.insert(s)), + (I::IntentToken(map), DbValueV1::IntentToken { u, s }) => { + let ts = match s { + DbValueIntentTokenStateV1::Valid => IntentTokenState::Valid, + DbValueIntentTokenStateV1::InProgress(i, d) => { + IntentTokenState::InProgress(i, d) + } + DbValueIntentTokenStateV1::Consumed => IntentTokenState::Consumed, + }; + + if let BTreeEntry::Vacant(e) = map.entry(u) { + e.insert(ts); + Ok(true) + } else { + Ok(false) + } + } + (I::TrustedDeviceEnrollment(map), DbValueV1::TrustedDeviceEnrollment { u }) => { + if let BTreeEntry::Vacant(e) = map.entry(u) { + e.insert(()); + Ok(true) + } else { + Ok(false) + } + } + (I::AuthSession(map), DbValueV1::AuthSession { u }) => { + if let BTreeEntry::Vacant(e) = map.entry(u) { + e.insert(()); + Ok(true) + } else { + Ok(false) + } + } (_, _) => Err(()), } .and_then(|is_new| { @@ -542,6 +619,15 @@ impl ValueSet { (I::RestrictedString(a), I::RestrictedString(b)) => { mergesets!(a, b) } + (I::IntentToken(a), I::IntentToken(b)) => { + mergemaps!(a, b) + } + (I::TrustedDeviceEnrollment(a), I::TrustedDeviceEnrollment(b)) => { + mergemaps!(a, b) + } + (I::AuthSession(a), I::AuthSession(b)) => { + mergemaps!(a, b) + } // I think that in this case, we need to specify self / everything as we are changing // type and we need to potentially purge everything, so we just return the left side. _ => Err(OperationError::InvalidValueState), @@ -652,6 +738,15 @@ impl ValueSet { I::RestrictedString(set) => { set.extend(iter.filter_map(|v| v.to_restrictedstring())); } + I::IntentToken(map) => { + map.extend(iter.filter_map(|v| v.to_intenttoken())); + } + I::TrustedDeviceEnrollment(map) => { + map.extend(iter.filter_map(|v| v.to_trusteddeviceenrollment())); + } + I::AuthSession(map) => { + map.extend(iter.filter_map(|v| v.to_authsession())); + } } } @@ -737,6 +832,15 @@ impl ValueSet { I::RestrictedString(set) => { set.clear(); } + I::IntentToken(map) => { + map.clear(); + } + I::TrustedDeviceEnrollment(map) => { + map.clear(); + } + I::AuthSession(map) => { + map.clear(); + } }; debug_assert!(self.is_empty()); } @@ -831,6 +935,15 @@ impl ValueSet { (I::RestrictedString(set), PartialValue::RestrictedString(s)) => { set.remove(s); } + (I::IntentToken(map), PartialValue::IntentToken(t)) => { + map.remove(t); + } + (I::TrustedDeviceEnrollment(map), PartialValue::TrustedDeviceEnrollment(t)) => { + map.remove(t); + } + (I::AuthSession(map), PartialValue::AuthSession(t)) => { + map.remove(t); + } (_, _) => { debug_assert!(false) } @@ -873,6 +986,11 @@ impl ValueSet { (I::RestrictedString(set), PartialValue::RestrictedString(s)) => { set.contains(s.as_str()) } + (I::IntentToken(map), PartialValue::IntentToken(u)) => map.contains_key(u), + (I::TrustedDeviceEnrollment(map), PartialValue::TrustedDeviceEnrollment(u)) => { + map.contains_key(u) + } + (I::AuthSession(map), PartialValue::AuthSession(u)) => map.contains_key(u), _ => false, } } @@ -925,6 +1043,9 @@ impl ValueSet { I::PrivateBinary(set) => set.len(), I::PublicBinary(map) => map.len(), I::RestrictedString(set) => set.len(), + I::IntentToken(map) => map.len(), + I::TrustedDeviceEnrollment(map) => map.len(), + I::AuthSession(map) => map.len(), } } @@ -986,6 +1107,18 @@ impl ValueSet { I::PrivateBinary(_set) => vec![], I::PublicBinary(map) => map.keys().cloned().collect(), I::RestrictedString(set) => set.iter().cloned().collect(), + I::IntentToken(map) => map + .keys() + .map(|u| u.to_hyphenated_ref().to_string()) + .collect(), + I::TrustedDeviceEnrollment(map) => map + .keys() + .map(|u| u.to_hyphenated_ref().to_string()) + .collect(), + I::AuthSession(map) => map + .keys() + .map(|u| u.to_hyphenated_ref().to_string()) + .collect(), } } @@ -1263,6 +1396,48 @@ impl ValueSet { }) } } + (I::IntentToken(a), I::IntentToken(b)) => { + let x: BTreeMap<_, _> = a + .iter() + .filter(|(k, _)| b.contains_key(k)) + .map(|(k, v)| (*k, v.clone())) + .collect(); + if x.is_empty() { + None + } else { + Some(ValueSet { + inner: I::IntentToken(x), + }) + } + } + (I::TrustedDeviceEnrollment(a), I::TrustedDeviceEnrollment(b)) => { + let x: BTreeMap<_, _> = a + .iter() + .filter(|(k, _)| b.contains_key(k)) + .map(|(k, v)| (*k, v.clone())) + .collect(); + if x.is_empty() { + None + } else { + Some(ValueSet { + inner: I::TrustedDeviceEnrollment(x), + }) + } + } + (I::AuthSession(a), I::AuthSession(b)) => { + let x: BTreeMap<_, _> = a + .iter() + .filter(|(k, _)| b.contains_key(k)) + .map(|(k, v)| (*k, v.clone())) + .collect(); + if x.is_empty() { + None + } else { + Some(ValueSet { + inner: I::AuthSession(x), + }) + } + } // I think that in this case, we need to specify self / everything as we are changing // type and we need to potentially purge everything, so we just return the left side. _ => Some(self.clone()), @@ -1550,6 +1725,13 @@ impl ValueSet { } } + pub fn as_intenttoken(&self) -> Option<&BTreeMap> { + match &self.inner { + I::IntentToken(map) => Some(map), + _ => None, + } + } + pub fn to_proto_string_clone_iter(&self) -> ProtoIter<'_> { // to_proto_string_clone match &self.inner { @@ -1579,6 +1761,9 @@ impl ValueSet { I::PrivateBinary(set) => ProtoIter::PrivateBinary(set.iter()), I::PublicBinary(set) => ProtoIter::PublicBinary(set.iter()), I::RestrictedString(set) => ProtoIter::RestrictedString(set.iter()), + I::IntentToken(map) => ProtoIter::IntentToken(map.iter()), + I::TrustedDeviceEnrollment(map) => ProtoIter::TrustedDeviceEnrollment(map.iter()), + I::AuthSession(map) => ProtoIter::AuthSession(map.iter()), } } @@ -1614,6 +1799,9 @@ impl ValueSet { I::PrivateBinary(set) => DbValueV1Iter::PrivateBinary(set.iter()), I::PublicBinary(set) => DbValueV1Iter::PublicBinary(set.iter()), I::RestrictedString(set) => DbValueV1Iter::RestrictedString(set.iter()), + I::IntentToken(map) => DbValueV1Iter::IntentToken(map.iter()), + I::TrustedDeviceEnrollment(map) => DbValueV1Iter::TrustedDeviceEnrollment(map.iter()), + I::AuthSession(map) => DbValueV1Iter::AuthSession(map.iter()), } } @@ -1645,6 +1833,11 @@ impl ValueSet { I::PrivateBinary(set) => PartialValueIter::PrivateBinary(set.iter()), I::PublicBinary(set) => PartialValueIter::PublicBinary(set.iter()), I::RestrictedString(set) => PartialValueIter::RestrictedString(set.iter()), + I::IntentToken(map) => PartialValueIter::IntentToken(map.iter()), + I::TrustedDeviceEnrollment(map) => { + PartialValueIter::TrustedDeviceEnrollment(map.iter()) + } + I::AuthSession(map) => PartialValueIter::AuthSession(map.iter()), } } @@ -1676,6 +1869,9 @@ impl ValueSet { I::PrivateBinary(set) => ValueIter::PrivateBinary(set.iter()), I::PublicBinary(set) => ValueIter::PublicBinary(set.iter()), I::RestrictedString(set) => ValueIter::RestrictedString(set.iter()), + I::IntentToken(map) => ValueIter::IntentToken(map.iter()), + I::TrustedDeviceEnrollment(map) => ValueIter::TrustedDeviceEnrollment(map.iter()), + I::AuthSession(map) => ValueIter::AuthSession(map.iter()), } } @@ -1808,6 +2004,10 @@ impl ValueSet { matches!(self.inner, I::PrivateBinary(_)) } + pub fn is_intenttoken(&self) -> bool { + matches!(self.inner, I::IntentToken(_)) + } + pub fn validate(&self) -> bool { match &self.inner { I::Iname(set) => set.iter().all(|s| { @@ -1906,6 +2106,9 @@ impl PartialEq for ValueSet { (I::PrivateBinary(a), I::PrivateBinary(b)) => a.eq(b), (I::PublicBinary(a), I::PublicBinary(b)) => a.eq(b), (I::RestrictedString(a), I::RestrictedString(b)) => a.eq(b), + (I::IntentToken(a), I::IntentToken(b)) => a.eq(b), + (I::TrustedDeviceEnrollment(a), I::TrustedDeviceEnrollment(b)) => a.eq(b), + (I::AuthSession(a), I::AuthSession(b)) => a.eq(b), _ => false, } } @@ -1973,6 +2176,9 @@ pub enum ValueIter<'a> { PrivateBinary(SmolSetIter<'a, [Vec; 1]>), PublicBinary(std::collections::btree_map::Iter<'a, String, Vec>), RestrictedString(std::collections::btree_set::Iter<'a, String>), + IntentToken(std::collections::btree_map::Iter<'a, Uuid, IntentTokenState>), + TrustedDeviceEnrollment(std::collections::btree_map::Iter<'a, Uuid, ()>), + AuthSession(std::collections::btree_map::Iter<'a, Uuid, ()>), } impl<'a> Iterator for ValueIter<'a> { @@ -2032,6 +2238,13 @@ impl<'a> Iterator for ValueIter<'a> { ValueIter::RestrictedString(iter) => { iter.next().map(|i| Value::new_restrictedstring(i.clone())) } + ValueIter::IntentToken(iter) => iter + .next() + .map(|(u, ts)| Value::IntentToken(*u, ts.clone())), + ValueIter::TrustedDeviceEnrollment(iter) => { + iter.next().map(|(u, _)| Value::TrustedDeviceEnrollment(*u)) + } + ValueIter::AuthSession(iter) => iter.next().map(|(u, _)| Value::AuthSession(*u)), } } } @@ -2063,6 +2276,9 @@ pub enum PartialValueIter<'a> { PrivateBinary(SmolSetIter<'a, [Vec; 1]>), PublicBinary(std::collections::btree_map::Iter<'a, String, Vec>), RestrictedString(std::collections::btree_set::Iter<'a, String>), + IntentToken(std::collections::btree_map::Iter<'a, Uuid, IntentTokenState>), + TrustedDeviceEnrollment(std::collections::btree_map::Iter<'a, Uuid, ()>), + AuthSession(std::collections::btree_map::Iter<'a, Uuid, ()>), } impl<'a> Iterator for PartialValueIter<'a> { @@ -2135,6 +2351,15 @@ impl<'a> Iterator for PartialValueIter<'a> { PartialValueIter::RestrictedString(iter) => iter .next() .map(|i| PartialValue::new_restrictedstring_s(i.as_str())), + PartialValueIter::IntentToken(iter) => { + iter.next().map(|(u, _state)| PartialValue::IntentToken(*u)) + } + PartialValueIter::TrustedDeviceEnrollment(iter) => iter + .next() + .map(|(u, _)| PartialValue::TrustedDeviceEnrollment(*u)), + PartialValueIter::AuthSession(iter) => { + iter.next().map(|(u, _)| PartialValue::AuthSession(*u)) + } } } } @@ -2172,6 +2397,9 @@ pub enum DbValueV1Iter<'a> { PrivateBinary(SmolSetIter<'a, [Vec; 1]>), PublicBinary(std::collections::btree_map::Iter<'a, String, Vec>), RestrictedString(std::collections::btree_set::Iter<'a, String>), + IntentToken(std::collections::btree_map::Iter<'a, Uuid, IntentTokenState>), + TrustedDeviceEnrollment(std::collections::btree_map::Iter<'a, Uuid, ()>), + AuthSession(std::collections::btree_map::Iter<'a, Uuid, ()>), } impl<'a> Iterator for DbValueV1Iter<'a> { @@ -2267,6 +2495,22 @@ impl<'a> Iterator for DbValueV1Iter<'a> { DbValueV1Iter::RestrictedString(iter) => { iter.next().map(|i| DbValueV1::RestrictedString(i.clone())) } + DbValueV1Iter::IntentToken(iter) => iter.next().map(|(u, s)| DbValueV1::IntentToken { + u: *u, + s: match s { + IntentTokenState::Valid => DbValueIntentTokenStateV1::Valid, + IntentTokenState::InProgress(i, d) => { + DbValueIntentTokenStateV1::InProgress(*i, *d) + } + IntentTokenState::Consumed => DbValueIntentTokenStateV1::Consumed, + }, + }), + DbValueV1Iter::TrustedDeviceEnrollment(iter) => iter + .next() + .map(|(u, _)| DbValueV1::TrustedDeviceEnrollment { u: *u }), + DbValueV1Iter::AuthSession(iter) => { + iter.next().map(|(u, _)| DbValueV1::AuthSession { u: *u }) + } } } } @@ -2298,6 +2542,9 @@ pub enum ProtoIter<'a> { PrivateBinary(SmolSetIter<'a, [Vec; 1]>), PublicBinary(std::collections::btree_map::Iter<'a, String, Vec>), RestrictedString(std::collections::btree_set::Iter<'a, String>), + IntentToken(std::collections::btree_map::Iter<'a, Uuid, IntentTokenState>), + TrustedDeviceEnrollment(std::collections::btree_map::Iter<'a, Uuid, ()>), + AuthSession(std::collections::btree_map::Iter<'a, Uuid, ()>), } impl<'a> Iterator for ProtoIter<'a> { @@ -2357,6 +2604,15 @@ impl<'a> Iterator for ProtoIter<'a> { .next() .map(|(t, b)| format!("{}: {}", t, base64::encode_config(&b, base64::URL_SAFE))), ProtoIter::RestrictedString(iter) => iter.next().cloned(), + ProtoIter::IntentToken(iter) => iter + .next() + .map(|(u, m)| format!("{}: {:?}", ValueSet::uuid_to_proto_string(u), m)), + ProtoIter::TrustedDeviceEnrollment(iter) => iter + .next() + .map(|(u, _)| format!("{}", ValueSet::uuid_to_proto_string(u))), + ProtoIter::AuthSession(iter) => iter + .next() + .map(|(u, _)| format!("{}", ValueSet::uuid_to_proto_string(u))), } } } diff --git a/kanidmd/score/Cargo.toml b/kanidmd/score/Cargo.toml index 967836b67..042d25a63 100644 --- a/kanidmd/score/Cargo.toml +++ b/kanidmd/score/Cargo.toml @@ -13,7 +13,7 @@ repository = "https://github.com/kanidm/kanidm/" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -kanidm = { path = "../" } +kanidm = { path = "../idm" } kanidm_proto = { path = "../../kanidm_proto" } libc = "0.2" @@ -37,3 +37,21 @@ compact_jwt = "^0.2.0" [build-dependencies] profiles = { path = "../../profiles" } + +[dev-dependencies] +tracing-subscriber = "0.3" +# tokio = { version = "1", features = ["rt", "net", "time", "macros", "sync", "signal"] } +# kanidm = { path = "../kanidmd" } +# score = { path = "../kanidmd/score" } +futures = "0.3" +serde_json = "1.0" +# async-std = { version = "1.6", features = ["tokio1"] } + +webauthn-authenticator-rs = "0.3.0-alpha.12" +oauth2_ext = { package = "oauth2", version = "4.0", default-features = false } +base64 = "0.13" +# compact_jwt = "^0.1.5" + +kanidm_client = { path = "../../kanidm_client" } +url = { version = "2", features = ["serde"] } +reqwest = { version = "0.11", features=["cookies", "json", "native-tls"] } diff --git a/kanidm_client/tests/common.rs b/kanidmd/score/tests/common.rs similarity index 100% rename from kanidm_client/tests/common.rs rename to kanidmd/score/tests/common.rs diff --git a/kanidm_client/tests/default_entries.rs b/kanidmd/score/tests/default_entries.rs similarity index 100% rename from kanidm_client/tests/default_entries.rs rename to kanidmd/score/tests/default_entries.rs diff --git a/kanidm_client/tests/oauth2_test.rs b/kanidmd/score/tests/oauth2_test.rs similarity index 100% rename from kanidm_client/tests/oauth2_test.rs rename to kanidmd/score/tests/oauth2_test.rs diff --git a/kanidm_client/tests/proto_v1_test.rs b/kanidmd/score/tests/proto_v1_test.rs similarity index 97% rename from kanidm_client/tests/proto_v1_test.rs rename to kanidmd/score/tests/proto_v1_test.rs index c90b35010..6c046d6a2 100644 --- a/kanidm_client/tests/proto_v1_test.rs +++ b/kanidmd/score/tests/proto_v1_test.rs @@ -374,27 +374,45 @@ fn test_server_radius_credential_lifecycle() { .idm_account_person_extend("admin", None, None) .unwrap(); + let f = Filter::Eq("name".to_string(), "idm_admins".to_string()); + let m = ModifyList::new_list(vec![Modify::Present( + "member".to_string(), + "system_admins".to_string(), + )]); + let res = rsclient.modify(f, m); + assert!(res.is_ok()); + + rsclient + .idm_account_create("demo_account", "Deeeeemo") + .unwrap(); + // Should have no radius secret - let n_sec = rsclient.idm_account_radius_credential_get("admin").unwrap(); + let n_sec = rsclient + .idm_account_radius_credential_get("demo_account") + .unwrap(); assert!(n_sec.is_none()); // Set one let sec1 = rsclient - .idm_account_radius_credential_regenerate("admin") + .idm_account_radius_credential_regenerate("demo_account") .unwrap(); // Should be able to get it. - let r_sec = rsclient.idm_account_radius_credential_get("admin").unwrap(); + let r_sec = rsclient + .idm_account_radius_credential_get("demo_account") + .unwrap(); assert!(sec1 == r_sec.unwrap()); // test getting the token - we can do this as self or the radius server - let r_tok = rsclient.idm_account_radius_token_get("admin").unwrap(); + let r_tok = rsclient + .idm_account_radius_token_get("demo_account") + .unwrap(); assert!(sec1 == r_tok.secret); - assert!(r_tok.name == "admin"); + assert!(r_tok.name == "demo_account"); // Reset it let sec2 = rsclient - .idm_account_radius_credential_regenerate("admin") + .idm_account_radius_credential_regenerate("demo_account") .unwrap(); // Should be different @@ -402,11 +420,13 @@ fn test_server_radius_credential_lifecycle() { assert!(sec1 != sec2); // Delete it - let res = rsclient.idm_account_radius_credential_delete("admin"); + let res = rsclient.idm_account_radius_credential_delete("demo_account"); assert!(res.is_ok()); // No secret - let n_sec = rsclient.idm_account_radius_credential_get("admin").unwrap(); + let n_sec = rsclient + .idm_account_radius_credential_get("demo_account") + .unwrap(); assert!(n_sec.is_none()); }); } diff --git a/kanidmd_web_ui/pkg/kanidmd_web_ui.d.ts b/kanidmd_web_ui/pkg/kanidmd_web_ui.d.ts new file mode 100644 index 000000000..232b97ee8 --- /dev/null +++ b/kanidmd_web_ui/pkg/kanidmd_web_ui.d.ts @@ -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__h79af8eb94e22cb43: (a: number, b: number, c: number) => void; + readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h1c17030d49aa8a7b: (a: number, b: number, c: number) => void; + readonly _dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hb210817f7291cc2b: (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} module_or_path +* +* @returns {Promise} +*/ +export default function init (module_or_path?: InitInput | Promise): Promise; diff --git a/kanidmd_web_ui/pkg/kanidmd_web_ui.js b/kanidmd_web_ui/pkg/kanidmd_web_ui.js index 5a1baa5cf..0e44dab0d 100644 --- a/kanidmd_web_ui/pkg/kanidmd_web_ui.js +++ b/kanidmd_web_ui/pkg/kanidmd_web_ui.js @@ -7,32 +7,7 @@ heap.push(undefined, null, true, false); function getObject(idx) { return heap[idx]; } -let heap_next = heap.length; - -function dropObject(idx) { - if (idx < 36) return; - heap[idx] = heap_next; - heap_next = idx; -} - -function takeObject(idx) { - const ret = getObject(idx); - dropObject(idx); - return ret; -} - -function addHeapObject(obj) { - if (heap_next === heap.length) heap.push(heap.length + 1); - const idx = heap_next; - heap_next = heap[idx]; - - heap[idx] = obj; - return idx; -} - -let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); - -cachedTextDecoder.decode(); +let WASM_VECTOR_LEN = 0; let cachegetUint8Memory0 = null; function getUint8Memory0() { @@ -42,12 +17,6 @@ function getUint8Memory0() { return cachegetUint8Memory0; } -function getStringFromWasm0(ptr, len) { - return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); -} - -let WASM_VECTOR_LEN = 0; - let cachedTextEncoder = new TextEncoder('utf-8'); const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' @@ -65,6 +34,8 @@ const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' function passStringToWasm0(arg, malloc, realloc) { + if (typeof(arg) !== 'string') throw new Error('expected a string argument'); + if (realloc === undefined) { const buf = cachedTextEncoder.encode(arg); const ptr = malloc(buf.length); @@ -93,7 +64,7 @@ function passStringToWasm0(arg, malloc, realloc) { ptr = realloc(ptr, len, len = offset + arg.length * 3); const view = getUint8Memory0().subarray(ptr + offset, ptr + len); const ret = encodeString(arg, view); - + if (ret.read !== arg.length) throw new Error('failed to pass whole string'); offset += ret.written; } @@ -101,10 +72,6 @@ function passStringToWasm0(arg, malloc, realloc) { return ptr; } -function isLikeNone(x) { - return x === undefined || x === null; -} - let cachegetInt32Memory0 = null; function getInt32Memory0() { if (cachegetInt32Memory0 === null || cachegetInt32Memory0.buffer !== wasm.memory.buffer) { @@ -113,6 +80,41 @@ function getInt32Memory0() { return cachegetInt32Memory0; } +let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + +cachedTextDecoder.decode(); + +function getStringFromWasm0(ptr, len) { + return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); +} + +let heap_next = heap.length; + +function addHeapObject(obj) { + if (heap_next === heap.length) heap.push(heap.length + 1); + const idx = heap_next; + heap_next = heap[idx]; + + if (typeof(heap_next) !== 'number') throw new Error('corrupt heap'); + + heap[idx] = obj; + return idx; +} + +function isLikeNone(x) { + return x === undefined || x === null; +} + +function _assertNum(n) { + if (typeof(n) !== 'number') throw new Error('expected a number argument'); +} + +function _assertBoolean(n) { + if (typeof(n) !== 'boolean') { + throw new Error('expected a boolean argument'); + } +} + let cachegetFloat64Memory0 = null; function getFloat64Memory0() { if (cachegetFloat64Memory0 === null || cachegetFloat64Memory0.buffer !== wasm.memory.buffer) { @@ -121,6 +123,18 @@ function getFloat64Memory0() { return cachegetFloat64Memory0; } +function dropObject(idx) { + if (idx < 36) return; + heap[idx] = heap_next; + heap_next = idx; +} + +function takeObject(idx) { + const ret = getObject(idx); + dropObject(idx); + return ret; +} + function debugString(val) { // primitive types const type = typeof val; @@ -186,6 +200,49 @@ function debugString(val) { return className; } +function makeClosure(arg0, arg1, dtor, f) { + const state = { a: arg0, b: arg1, cnt: 1, dtor }; + const real = (...args) => { + // First up with a closure we increment the internal reference + // count. This ensures that the Rust closure environment won't + // be deallocated while we're invoking it. + state.cnt++; + try { + return f(state.a, state.b, ...args); + } finally { + if (--state.cnt === 0) { + wasm.__wbindgen_export_2.get(state.dtor)(state.a, state.b); + state.a = 0; + + } + } + }; + real.original = state; + + return real; +} + +function logError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + let error = (function () { + try { + return e instanceof Error ? `${e.message}\n\nStack:\n${e.stack}` : e.toString(); + } catch(_) { + return ""; + } + }()); + console.error("wasm-bindgen: imported JS function that was not marked as `catch` threw an error:", error); + throw e; + } +} +function __wbg_adapter_30(arg0, arg1, arg2) { + _assertNum(arg0); + _assertNum(arg1); + wasm._dyn_core__ops__function__Fn__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h79af8eb94e22cb43(arg0, arg1, addHeapObject(arg2)); +} + function makeMutClosure(arg0, arg1, dtor, f) { const state = { a: arg0, b: arg1, cnt: 1, dtor }; const real = (...args) => { @@ -210,6 +267,11 @@ function makeMutClosure(arg0, arg1, dtor, f) { return real; } +function __wbg_adapter_33(arg0, arg1, arg2) { + _assertNum(arg0); + _assertNum(arg1); + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h1c17030d49aa8a7b(arg0, arg1, addHeapObject(arg2)); +} let stack_pointer = 32; @@ -218,43 +280,16 @@ function addBorrowedObject(obj) { heap[--stack_pointer] = obj; return stack_pointer; } -function __wbg_adapter_30(arg0, arg1, arg2) { +function __wbg_adapter_36(arg0, arg1, arg2) { try { - wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h044f18bcb347ffd0(arg0, arg1, addBorrowedObject(arg2)); + _assertNum(arg0); + _assertNum(arg1); + wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hb210817f7291cc2b(arg0, arg1, addBorrowedObject(arg2)); } finally { heap[stack_pointer++] = undefined; } } -function makeClosure(arg0, arg1, dtor, f) { - const state = { a: arg0, b: arg1, cnt: 1, dtor }; - const real = (...args) => { - // First up with a closure we increment the internal reference - // count. This ensures that the Rust closure environment won't - // be deallocated while we're invoking it. - state.cnt++; - try { - return f(state.a, state.b, ...args); - } finally { - if (--state.cnt === 0) { - wasm.__wbindgen_export_2.get(state.dtor)(state.a, state.b); - state.a = 0; - - } - } - }; - real.original = state; - - return real; -} -function __wbg_adapter_33(arg0, arg1, arg2) { - wasm._dyn_core__ops__function__Fn__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__ha821c5ee02284a59(arg0, arg1, addHeapObject(arg2)); -} - -function __wbg_adapter_36(arg0, arg1, arg2) { - wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hb14a7cff63e25e7d(arg0, arg1, addHeapObject(arg2)); -} - /** */ export function run_app() { @@ -334,12 +369,13 @@ async function init(input) { } const imports = {}; imports.wbg = {}; - imports.wbg.__wbindgen_object_drop_ref = function(arg0) { - takeObject(arg0); - }; - imports.wbg.__wbindgen_object_clone_ref = function(arg0) { - var ret = getObject(arg0); - return addHeapObject(ret); + imports.wbg.__wbindgen_json_serialize = function(arg0, arg1) { + const obj = getObject(arg1); + var ret = JSON.stringify(obj === undefined ? null : obj); + var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; }; imports.wbg.__wbindgen_string_new = function(arg0, arg1) { var ret = getStringFromWasm0(arg0, arg1); @@ -353,36 +389,25 @@ async function init(input) { getInt32Memory0()[arg0 / 4 + 1] = len0; getInt32Memory0()[arg0 / 4 + 0] = ptr0; }; - imports.wbg.__wbindgen_json_serialize = function(arg0, arg1) { - const obj = getObject(arg1); - var ret = JSON.stringify(obj === undefined ? null : obj); - var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - var len0 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len0; - getInt32Memory0()[arg0 / 4 + 0] = ptr0; - }; - imports.wbg.__wbindgen_cb_drop = function(arg0) { - const obj = takeObject(arg0).original; - if (obj.cnt-- == 1) { - obj.a = 0; - return true; - } - var ret = false; - return ret; - }; - imports.wbg.__wbindgen_boolean_get = function(arg0) { - const v = getObject(arg0); - var ret = typeof(v) === 'boolean' ? (v ? 1 : 0) : 2; - return ret; - }; - imports.wbg.__wbindgen_is_undefined = function(arg0) { - var ret = getObject(arg0) === undefined; - return ret; + imports.wbg.__wbindgen_object_clone_ref = function(arg0) { + var ret = getObject(arg0); + return addHeapObject(ret); }; imports.wbg.__wbindgen_json_parse = function(arg0, arg1) { var ret = JSON.parse(getStringFromWasm0(arg0, arg1)); return addHeapObject(ret); }; + imports.wbg.__wbindgen_boolean_get = function(arg0) { + const v = getObject(arg0); + var ret = typeof(v) === 'boolean' ? (v ? 1 : 0) : 2; + _assertNum(ret); + return ret; + }; + imports.wbg.__wbindgen_is_undefined = function(arg0) { + var ret = getObject(arg0) === undefined; + _assertBoolean(ret); + return ret; + }; imports.wbg.__wbindgen_number_new = function(arg0) { var ret = arg0; return addHeapObject(ret); @@ -390,57 +415,74 @@ async function init(input) { imports.wbg.__wbindgen_number_get = function(arg0, arg1) { const obj = getObject(arg1); var ret = typeof(obj) === 'number' ? obj : undefined; + if (!isLikeNone(ret)) { + _assertNum(ret); + } getFloat64Memory0()[arg0 / 8 + 1] = isLikeNone(ret) ? 0 : ret; getInt32Memory0()[arg0 / 4 + 0] = !isLikeNone(ret); }; - imports.wbg.__wbg_new_693216e109162396 = function() { - var ret = new Error(); - return addHeapObject(ret); + imports.wbg.__wbindgen_object_drop_ref = function(arg0) { + takeObject(arg0); }; - imports.wbg.__wbg_stack_0ddaca5d1abfb52f = function(arg0, arg1) { - var ret = getObject(arg1).stack; - var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - var len0 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len0; - getInt32Memory0()[arg0 / 4 + 0] = ptr0; - }; - imports.wbg.__wbg_error_09919627ac0992f5 = function(arg0, arg1) { + imports.wbg.__wbg_error_09919627ac0992f5 = function() { return logError(function (arg0, arg1) { try { console.error(getStringFromWasm0(arg0, arg1)); } finally { wasm.__wbindgen_free(arg0, arg1); } + }, arguments) }; + imports.wbg.__wbg_new_693216e109162396 = function() { return logError(function () { + var ret = new Error(); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_stack_0ddaca5d1abfb52f = function() { return logError(function (arg0, arg1) { + var ret = getObject(arg1).stack; + var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }, arguments) }; + imports.wbg.__wbindgen_cb_drop = function(arg0) { + const obj = takeObject(arg0).original; + if (obj.cnt-- == 1) { + obj.a = 0; + return true; + } + var ret = false; + _assertBoolean(ret); + return ret; }; - imports.wbg.__wbg_log_06b7ffc63a0f8bee = function(arg0, arg1) { + imports.wbg.__wbg_log_06b7ffc63a0f8bee = function() { return logError(function (arg0, arg1) { var v0 = getArrayJsValueFromWasm0(arg0, arg1).slice(); wasm.__wbindgen_free(arg0, arg1 * 4); console.log(...v0); - }; - imports.wbg.__wbg_warn_2aa0e7178e1d35f6 = function(arg0, arg1) { + }, arguments) }; + imports.wbg.__wbg_warn_2aa0e7178e1d35f6 = function() { return logError(function (arg0, arg1) { var v0 = getArrayJsValueFromWasm0(arg0, arg1).slice(); wasm.__wbindgen_free(arg0, arg1 * 4); console.warn(...v0); - }; - imports.wbg.__wbg_instanceof_Window_434ce1849eb4e0fc = function(arg0) { + }, arguments) }; + imports.wbg.__wbg_instanceof_Window_434ce1849eb4e0fc = function() { return logError(function (arg0) { var ret = getObject(arg0) instanceof Window; + _assertBoolean(ret); return ret; - }; - imports.wbg.__wbg_document_5edd43643d1060d9 = function(arg0) { + }, arguments) }; + imports.wbg.__wbg_document_5edd43643d1060d9 = function() { return logError(function (arg0) { var ret = getObject(arg0).document; return isLikeNone(ret) ? 0 : addHeapObject(ret); - }; - imports.wbg.__wbg_location_11472bb76bf5bbca = function(arg0) { + }, arguments) }; + imports.wbg.__wbg_location_11472bb76bf5bbca = function() { return logError(function (arg0) { var ret = getObject(arg0).location; return addHeapObject(ret); - }; + }, arguments) }; imports.wbg.__wbg_history_52cfc93c824e772b = function() { return handleError(function (arg0) { var ret = getObject(arg0).history; return addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_navigator_0e0588c949560476 = function(arg0) { + imports.wbg.__wbg_navigator_0e0588c949560476 = function() { return logError(function (arg0) { var ret = getObject(arg0).navigator; return addHeapObject(ret); - }; + }, arguments) }; imports.wbg.__wbg_localStorage_2b7091e6919605e2 = function() { return handleError(function (arg0) { var ret = getObject(arg0).localStorage; return isLikeNone(ret) ? 0 : addHeapObject(ret); @@ -449,14 +491,14 @@ async function init(input) { var ret = getObject(arg0).sessionStorage; return isLikeNone(ret) ? 0 : addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_fetch_427498e0ccea81f4 = function(arg0, arg1) { + imports.wbg.__wbg_fetch_427498e0ccea81f4 = function() { return logError(function (arg0, arg1) { var ret = getObject(arg0).fetch(getObject(arg1)); return addHeapObject(ret); - }; - imports.wbg.__wbg_body_7538539844356c1c = function(arg0) { + }, arguments) }; + imports.wbg.__wbg_body_7538539844356c1c = function() { return logError(function (arg0) { var ret = getObject(arg0).body; return isLikeNone(ret) ? 0 : addHeapObject(ret); - }; + }, arguments) }; imports.wbg.__wbg_createElement_d017b8d2af99bab9 = function() { return handleError(function (arg0, arg1, arg2) { var ret = getObject(arg0).createElement(getStringFromWasm0(arg1, arg2)); return addHeapObject(ret); @@ -465,89 +507,102 @@ async function init(input) { var ret = getObject(arg0).createElementNS(arg1 === 0 ? undefined : getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); return addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_createTextNode_39a0de25d14bcde5 = function(arg0, arg1, arg2) { + imports.wbg.__wbg_createTextNode_39a0de25d14bcde5 = function() { return logError(function (arg0, arg1, arg2) { var ret = getObject(arg0).createTextNode(getStringFromWasm0(arg1, arg2)); return addHeapObject(ret); - }; - imports.wbg.__wbg_getElementById_b30e88aff96f66a1 = function(arg0, arg1, arg2) { + }, arguments) }; + imports.wbg.__wbg_getElementById_b30e88aff96f66a1 = function() { return logError(function (arg0, arg1, arg2) { var ret = getObject(arg0).getElementById(getStringFromWasm0(arg1, arg2)); return isLikeNone(ret) ? 0 : addHeapObject(ret); - }; + }, arguments) }; imports.wbg.__wbg_querySelector_cc714d0aa0b868ed = function() { return handleError(function (arg0, arg1, arg2) { var ret = getObject(arg0).querySelector(getStringFromWasm0(arg1, arg2)); return isLikeNone(ret) ? 0 : addHeapObject(ret); }, arguments) }; + imports.wbg.__wbg_pathname_7affbcff36f35c0e = function() { return logError(function (arg0, arg1) { + var ret = getObject(arg1).pathname; + var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }, arguments) }; + imports.wbg.__wbg_new_4473c9af1cac368b = function() { return handleError(function (arg0, arg1) { + var ret = new URL(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_value_d3a30bc2c7caf357 = function() { return logError(function (arg0, arg1) { + var ret = getObject(arg1).value; + var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }, arguments) }; + imports.wbg.__wbg_setvalue_6a34bab301f38bf2 = function() { return logError(function (arg0, arg1, arg2) { + getObject(arg0).value = getStringFromWasm0(arg1, arg2); + }, arguments) }; + imports.wbg.__wbg_get_5835a17331a9d8f2 = function() { return handleError(function (arg0, arg1) { + var ret = getObject(arg0).get(getObject(arg1)); + return addHeapObject(ret); + }, arguments) }; imports.wbg.__wbg_add_c1e566b20be6badb = function() { return handleError(function (arg0, arg1, arg2) { getObject(arg0).add(getStringFromWasm0(arg1, arg2)); }, arguments) }; imports.wbg.__wbg_remove_b4d29ca5eb7db54e = function() { return handleError(function (arg0, arg1, arg2) { getObject(arg0).remove(getStringFromWasm0(arg1, arg2)); }, arguments) }; - imports.wbg.__wbg_href_cad8f02caf39f2fb = function(arg0, arg1) { + imports.wbg.__wbg_href_cad8f02caf39f2fb = function() { return logError(function (arg0, arg1) { var ret = getObject(arg1).href; var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); var len0 = WASM_VECTOR_LEN; getInt32Memory0()[arg0 / 4 + 1] = len0; getInt32Memory0()[arg0 / 4 + 0] = ptr0; - }; - imports.wbg.__wbg_getClientExtensionResults_37549795564cd9d3 = function(arg0) { - var ret = getObject(arg0).getClientExtensionResults(); - return addHeapObject(ret); - }; - imports.wbg.__wbg_headers_1a60dec7fbd28a3b = function(arg0) { + }, arguments) }; + imports.wbg.__wbg_headers_1a60dec7fbd28a3b = function() { return logError(function (arg0) { var ret = getObject(arg0).headers; return addHeapObject(ret); - }; + }, arguments) }; imports.wbg.__wbg_newwithstrandinit_c07f0662ece15bc6 = function() { return handleError(function (arg0, arg1, arg2) { var ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2)); return addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_instanceof_HtmlInputElement_8969541a2a0bded0 = function(arg0) { - var ret = getObject(arg0) instanceof HTMLInputElement; + imports.wbg.__wbg_instanceof_Event_39e54e1fe6593f4c = function() { return logError(function (arg0) { + var ret = getObject(arg0) instanceof Event; + _assertBoolean(ret); return ret; - }; - imports.wbg.__wbg_setchecked_f6ead3490df88a7f = function(arg0, arg1) { + }, arguments) }; + imports.wbg.__wbg_target_e560052e31e4567c = function() { return logError(function (arg0) { + var ret = getObject(arg0).target; + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_cancelBubble_17d7988ab2fbe4c9 = function() { return logError(function (arg0) { + var ret = getObject(arg0).cancelBubble; + _assertBoolean(ret); + return ret; + }, arguments) }; + imports.wbg.__wbg_preventDefault_fa00541ff125b78c = function() { return logError(function (arg0) { + getObject(arg0).preventDefault(); + }, arguments) }; + imports.wbg.__wbg_getClientExtensionResults_37549795564cd9d3 = function() { return logError(function (arg0) { + var ret = getObject(arg0).getClientExtensionResults(); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_instanceof_HtmlInputElement_8969541a2a0bded0 = function() { return logError(function (arg0) { + var ret = getObject(arg0) instanceof HTMLInputElement; + _assertBoolean(ret); + return ret; + }, arguments) }; + imports.wbg.__wbg_setchecked_f6ead3490df88a7f = function() { return logError(function (arg0, arg1) { getObject(arg0).checked = arg1 !== 0; - }; - imports.wbg.__wbg_value_fc1c354d1a0e9714 = function(arg0, arg1) { + }, arguments) }; + imports.wbg.__wbg_value_fc1c354d1a0e9714 = function() { return logError(function (arg0, arg1) { var ret = getObject(arg1).value; var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); var len0 = WASM_VECTOR_LEN; getInt32Memory0()[arg0 / 4 + 1] = len0; getInt32Memory0()[arg0 / 4 + 0] = ptr0; - }; - imports.wbg.__wbg_setvalue_ce4a23f487065c07 = function(arg0, arg1, arg2) { - getObject(arg0).value = getStringFromWasm0(arg1, arg2); - }; - imports.wbg.__wbg_get_5835a17331a9d8f2 = function() { return handleError(function (arg0, arg1) { - var ret = getObject(arg0).get(getObject(arg1)); - return addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_instanceof_Event_39e54e1fe6593f4c = function(arg0) { - var ret = getObject(arg0) instanceof Event; - return ret; - }; - imports.wbg.__wbg_target_e560052e31e4567c = function(arg0) { - var ret = getObject(arg0).target; - return isLikeNone(ret) ? 0 : addHeapObject(ret); - }; - imports.wbg.__wbg_cancelBubble_17d7988ab2fbe4c9 = function(arg0) { - var ret = getObject(arg0).cancelBubble; - return ret; - }; - imports.wbg.__wbg_preventDefault_fa00541ff125b78c = function(arg0) { - getObject(arg0).preventDefault(); - }; - imports.wbg.__wbg_instanceof_HtmlDocument_395ec6365cabde6c = function(arg0) { - var ret = getObject(arg0) instanceof HTMLDocument; - return ret; - }; - imports.wbg.__wbg_cookie_66f4449cc764fcb2 = function() { return handleError(function (arg0, arg1) { - var ret = getObject(arg1).cookie; - var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - var len0 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len0; - getInt32Memory0()[arg0 / 4 + 0] = ptr0; + imports.wbg.__wbg_setvalue_ce4a23f487065c07 = function() { return logError(function (arg0, arg1, arg2) { + getObject(arg0).value = getStringFromWasm0(arg1, arg2); }, arguments) }; imports.wbg.__wbg_addEventListener_55682f77717d7665 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { getObject(arg0).addEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3), getObject(arg4)); @@ -555,64 +610,55 @@ async function init(input) { imports.wbg.__wbg_removeEventListener_9cd36e5806463d5d = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { getObject(arg0).removeEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3), arg4 !== 0); }, arguments) }; - imports.wbg.__wbg_credentials_403bf2de10e8f1c3 = function(arg0) { - var ret = getObject(arg0).credentials; - return addHeapObject(ret); - }; - imports.wbg.__wbg_instanceof_Element_c9423704dd5d9b1d = function(arg0) { - var ret = getObject(arg0) instanceof Element; + imports.wbg.__wbg_instanceof_HtmlDocument_395ec6365cabde6c = function() { return logError(function (arg0) { + var ret = getObject(arg0) instanceof HTMLDocument; + _assertBoolean(ret); return ret; - }; - imports.wbg.__wbg_namespaceURI_e9a971e6c1ce68db = function(arg0, arg1) { + }, arguments) }; + imports.wbg.__wbg_cookie_66f4449cc764fcb2 = function() { return handleError(function (arg0, arg1) { + var ret = getObject(arg1).cookie; + var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }, arguments) }; + imports.wbg.__wbg_instanceof_Element_c9423704dd5d9b1d = function() { return logError(function (arg0) { + var ret = getObject(arg0) instanceof Element; + _assertBoolean(ret); + return ret; + }, arguments) }; + imports.wbg.__wbg_namespaceURI_e9a971e6c1ce68db = function() { return logError(function (arg0, arg1) { var ret = getObject(arg1).namespaceURI; var ptr0 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); var len0 = WASM_VECTOR_LEN; getInt32Memory0()[arg0 / 4 + 1] = len0; getInt32Memory0()[arg0 / 4 + 0] = ptr0; - }; - imports.wbg.__wbg_classList_5086913f676eb3f3 = function(arg0) { + }, arguments) }; + imports.wbg.__wbg_classList_5086913f676eb3f3 = function() { return logError(function (arg0) { var ret = getObject(arg0).classList; return addHeapObject(ret); - }; + }, arguments) }; imports.wbg.__wbg_removeAttribute_1adaecf6b4d35a09 = function() { return handleError(function (arg0, arg1, arg2) { getObject(arg0).removeAttribute(getStringFromWasm0(arg1, arg2)); }, arguments) }; imports.wbg.__wbg_setAttribute_1776fcc9b98d464e = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { getObject(arg0).setAttribute(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); }, arguments) }; - imports.wbg.__wbg_instanceof_HtmlElement_d3e8f1c1d6788b24 = function(arg0) { + imports.wbg.__wbg_instanceof_HtmlElement_d3e8f1c1d6788b24 = function() { return logError(function (arg0) { var ret = getObject(arg0) instanceof HTMLElement; + _assertBoolean(ret); return ret; - }; + }, arguments) }; imports.wbg.__wbg_focus_4434360545ac99cf = function() { return handleError(function (arg0) { getObject(arg0).focus(); }, arguments) }; + imports.wbg.__wbg_credentials_403bf2de10e8f1c3 = function() { return logError(function (arg0) { + var ret = getObject(arg0).credentials; + return addHeapObject(ret); + }, arguments) }; imports.wbg.__wbg_pushState_89ce908020e1d6aa = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5) { getObject(arg0).pushState(getObject(arg1), getStringFromWasm0(arg2, arg3), arg4 === 0 ? undefined : getStringFromWasm0(arg4, arg5)); }, arguments) }; - imports.wbg.__wbg_parentElement_96e1e07348340043 = function(arg0) { - var ret = getObject(arg0).parentElement; - return isLikeNone(ret) ? 0 : addHeapObject(ret); - }; - imports.wbg.__wbg_lastChild_e2b014abab089e08 = function(arg0) { - var ret = getObject(arg0).lastChild; - return isLikeNone(ret) ? 0 : addHeapObject(ret); - }; - imports.wbg.__wbg_setnodeValue_f175b74a390f8fda = function(arg0, arg1, arg2) { - getObject(arg0).nodeValue = arg1 === 0 ? undefined : getStringFromWasm0(arg1, arg2); - }; - imports.wbg.__wbg_appendChild_3fe5090c665d3bb4 = function() { return handleError(function (arg0, arg1) { - var ret = getObject(arg0).appendChild(getObject(arg1)); - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbg_insertBefore_4f09909023feac91 = function() { return handleError(function (arg0, arg1, arg2) { - var ret = getObject(arg0).insertBefore(getObject(arg1), getObject(arg2)); - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbg_removeChild_f4a83c9698136bbb = function() { return handleError(function (arg0, arg1) { - var ret = getObject(arg0).removeChild(getObject(arg1)); - return addHeapObject(ret); - }, arguments) }; imports.wbg.__wbg_pathname_d0014089875ea691 = function() { return handleError(function (arg0, arg1) { var ret = getObject(arg1).pathname; var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); @@ -630,6 +676,39 @@ async function init(input) { imports.wbg.__wbg_replace_ec236a3e3182c4da = function() { return handleError(function (arg0, arg1, arg2) { getObject(arg0).replace(getStringFromWasm0(arg1, arg2)); }, arguments) }; + imports.wbg.__wbg_parentElement_96e1e07348340043 = function() { return logError(function (arg0) { + var ret = getObject(arg0).parentElement; + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_lastChild_e2b014abab089e08 = function() { return logError(function (arg0) { + var ret = getObject(arg0).lastChild; + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_setnodeValue_f175b74a390f8fda = function() { return logError(function (arg0, arg1, arg2) { + getObject(arg0).nodeValue = arg1 === 0 ? undefined : getStringFromWasm0(arg1, arg2); + }, arguments) }; + imports.wbg.__wbg_appendChild_3fe5090c665d3bb4 = function() { return handleError(function (arg0, arg1) { + var ret = getObject(arg0).appendChild(getObject(arg1)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_insertBefore_4f09909023feac91 = function() { return handleError(function (arg0, arg1, arg2) { + var ret = getObject(arg0).insertBefore(getObject(arg1), getObject(arg2)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_removeChild_f4a83c9698136bbb = function() { return handleError(function (arg0, arg1) { + var ret = getObject(arg0).removeChild(getObject(arg1)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_get_bee69a0c35eec41c = function() { return handleError(function (arg0, arg1, arg2, arg3) { + var ret = getObject(arg1).get(getStringFromWasm0(arg2, arg3)); + var ptr0 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }, arguments) }; + imports.wbg.__wbg_set_f9448486a94c9aef = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).set(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); + }, arguments) }; imports.wbg.__wbg_getItem_f92ef607397e96b1 = function() { return handleError(function (arg0, arg1, arg2, arg3) { var ret = getObject(arg1).getItem(getStringFromWasm0(arg2, arg3)); var ptr0 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); @@ -643,49 +722,20 @@ async function init(input) { imports.wbg.__wbg_setItem_279b13e5ad0b82cb = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { getObject(arg0).setItem(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); }, arguments) }; - imports.wbg.__wbg_get_bee69a0c35eec41c = function() { return handleError(function (arg0, arg1, arg2, arg3) { - var ret = getObject(arg1).get(getStringFromWasm0(arg2, arg3)); - var ptr0 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - var len0 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len0; - getInt32Memory0()[arg0 / 4 + 0] = ptr0; - }, arguments) }; - imports.wbg.__wbg_set_f9448486a94c9aef = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { - getObject(arg0).set(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); - }, arguments) }; - imports.wbg.__wbg_pathname_7affbcff36f35c0e = function(arg0, arg1) { - var ret = getObject(arg1).pathname; - var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - var len0 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len0; - getInt32Memory0()[arg0 / 4 + 0] = ptr0; - }; - imports.wbg.__wbg_new_4473c9af1cac368b = function() { return handleError(function (arg0, arg1) { - var ret = new URL(getStringFromWasm0(arg0, arg1)); - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbg_value_d3a30bc2c7caf357 = function(arg0, arg1) { - var ret = getObject(arg1).value; - var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - var len0 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len0; - getInt32Memory0()[arg0 / 4 + 0] = ptr0; - }; - imports.wbg.__wbg_setvalue_6a34bab301f38bf2 = function(arg0, arg1, arg2) { - getObject(arg0).value = getStringFromWasm0(arg1, arg2); - }; - imports.wbg.__wbg_instanceof_Response_ea36d565358a42f7 = function(arg0) { + imports.wbg.__wbg_instanceof_Response_ea36d565358a42f7 = function() { return logError(function (arg0) { var ret = getObject(arg0) instanceof Response; + _assertBoolean(ret); return ret; - }; - imports.wbg.__wbg_status_3a55bb50e744b834 = function(arg0) { + }, arguments) }; + imports.wbg.__wbg_status_3a55bb50e744b834 = function() { return logError(function (arg0) { var ret = getObject(arg0).status; + _assertNum(ret); return ret; - }; - imports.wbg.__wbg_headers_e4204c6775f7b3b4 = function(arg0) { + }, arguments) }; + imports.wbg.__wbg_headers_e4204c6775f7b3b4 = function() { return logError(function (arg0) { var ret = getObject(arg0).headers; return addHeapObject(ret); - }; + }, arguments) }; imports.wbg.__wbg_json_4ab99130d1a5b3a9 = function() { return handleError(function (arg0) { var ret = getObject(arg0).json(); return addHeapObject(ret); @@ -694,66 +744,69 @@ async function init(input) { var ret = getObject(arg0).text(); return addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_new_16f24b0728c5e67b = function() { + imports.wbg.__wbg_new_16f24b0728c5e67b = function() { return logError(function () { var ret = new Array(); return addHeapObject(ret); - }; - imports.wbg.__wbg_newnoargs_f579424187aa1717 = function(arg0, arg1) { - var ret = new Function(getStringFromWasm0(arg0, arg1)); + }, arguments) }; + imports.wbg.__wbg_push_a72df856079e6930 = function() { return logError(function (arg0, arg1) { + var ret = getObject(arg0).push(getObject(arg1)); + _assertNum(ret); + return ret; + }, arguments) }; + imports.wbg.__wbg_instanceof_Error_4287ce7d75f0e3a2 = function() { return logError(function (arg0) { + var ret = getObject(arg0) instanceof Error; + _assertBoolean(ret); + return ret; + }, arguments) }; + imports.wbg.__wbg_message_1dfe93b595be8811 = function() { return logError(function (arg0) { + var ret = getObject(arg0).message; return addHeapObject(ret); - }; - imports.wbg.__wbg_get_8bbb82393651dd9c = function() { return handleError(function (arg0, arg1) { - var ret = Reflect.get(getObject(arg0), getObject(arg1)); + }, arguments) }; + imports.wbg.__wbg_name_66305ab387468967 = function() { return logError(function (arg0) { + var ret = getObject(arg0).name; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_toString_3e854a6a919f2996 = function() { return logError(function (arg0) { + var ret = getObject(arg0).toString(); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_newnoargs_f579424187aa1717 = function() { return logError(function (arg0, arg1) { + var ret = new Function(getStringFromWasm0(arg0, arg1)); return addHeapObject(ret); }, arguments) }; imports.wbg.__wbg_call_89558c3e96703ca1 = function() { return handleError(function (arg0, arg1) { var ret = getObject(arg0).call(getObject(arg1)); return addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_new_d3138911a89329b0 = function() { - var ret = new Object(); - return addHeapObject(ret); - }; - imports.wbg.__wbg_push_a72df856079e6930 = function(arg0, arg1) { - var ret = getObject(arg0).push(getObject(arg1)); - return ret; - }; - imports.wbg.__wbg_instanceof_Error_4287ce7d75f0e3a2 = function(arg0) { - var ret = getObject(arg0) instanceof Error; - return ret; - }; - imports.wbg.__wbg_message_1dfe93b595be8811 = function(arg0) { - var ret = getObject(arg0).message; - return addHeapObject(ret); - }; - imports.wbg.__wbg_name_66305ab387468967 = function(arg0) { - var ret = getObject(arg0).name; - return addHeapObject(ret); - }; - imports.wbg.__wbg_toString_3e854a6a919f2996 = function(arg0) { - var ret = getObject(arg0).toString(); - return addHeapObject(ret); - }; - imports.wbg.__wbg_valueOf_39e0d6bc7e4232b9 = function(arg0) { + imports.wbg.__wbg_valueOf_39e0d6bc7e4232b9 = function() { return logError(function (arg0) { var ret = getObject(arg0).valueOf(); return ret; - }; - imports.wbg.__wbg_is_3d73f4d91adacc37 = function(arg0, arg1) { + }, arguments) }; + imports.wbg.__wbg_is_3d73f4d91adacc37 = function() { return logError(function (arg0, arg1) { var ret = Object.is(getObject(arg0), getObject(arg1)); + _assertBoolean(ret); return ret; - }; - imports.wbg.__wbg_resolve_4f8f547f26b30b27 = function(arg0) { + }, arguments) }; + imports.wbg.__wbg_new_d3138911a89329b0 = function() { return logError(function () { + var ret = new Object(); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_resolve_4f8f547f26b30b27 = function() { return logError(function (arg0) { var ret = Promise.resolve(getObject(arg0)); return addHeapObject(ret); - }; - imports.wbg.__wbg_then_a6860c82b90816ca = function(arg0, arg1) { + }, arguments) }; + imports.wbg.__wbg_then_a6860c82b90816ca = function() { return logError(function (arg0, arg1) { var ret = getObject(arg0).then(getObject(arg1)); return addHeapObject(ret); - }; - imports.wbg.__wbg_then_58a04e42527f52c6 = function(arg0, arg1, arg2) { + }, arguments) }; + imports.wbg.__wbg_then_58a04e42527f52c6 = function() { return logError(function (arg0, arg1, arg2) { var ret = getObject(arg0).then(getObject(arg1), getObject(arg2)); return addHeapObject(ret); - }; + }, arguments) }; + imports.wbg.__wbg_globalThis_d61b1f48a57191ae = function() { return handleError(function () { + var ret = globalThis.globalThis; + return addHeapObject(ret); + }, arguments) }; imports.wbg.__wbg_self_e23d74ae45fb17d1 = function() { return handleError(function () { var ret = self.self; return addHeapObject(ret); @@ -762,35 +815,37 @@ async function init(input) { var ret = window.window; return addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_globalThis_d61b1f48a57191ae = function() { return handleError(function () { - var ret = globalThis.globalThis; - return addHeapObject(ret); - }, arguments) }; imports.wbg.__wbg_global_e7669da72fd7f239 = function() { return handleError(function () { var ret = global.global; return addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_buffer_5e74a88a1424a2e0 = function(arg0) { - var ret = getObject(arg0).buffer; - return addHeapObject(ret); - }; - imports.wbg.__wbg_newwithbyteoffsetandlength_278ec7532799393a = function(arg0, arg1, arg2) { - var ret = new Uint8Array(getObject(arg0), arg1 >>> 0, arg2 >>> 0); - return addHeapObject(ret); - }; - imports.wbg.__wbg_new_e3b800e570795b3c = function(arg0) { + imports.wbg.__wbg_new_e3b800e570795b3c = function() { return logError(function (arg0) { var ret = new Uint8Array(getObject(arg0)); return addHeapObject(ret); - }; - imports.wbg.__wbg_set_5b8081e9d002f0df = function(arg0, arg1, arg2) { - getObject(arg0).set(getObject(arg1), arg2 >>> 0); - }; - imports.wbg.__wbg_length_30803400a8f15c59 = function(arg0) { + }, arguments) }; + imports.wbg.__wbg_newwithbyteoffsetandlength_278ec7532799393a = function() { return logError(function (arg0, arg1, arg2) { + var ret = new Uint8Array(getObject(arg0), arg1 >>> 0, arg2 >>> 0); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_length_30803400a8f15c59 = function() { return logError(function (arg0) { var ret = getObject(arg0).length; + _assertNum(ret); return ret; - }; + }, arguments) }; + imports.wbg.__wbg_set_5b8081e9d002f0df = function() { return logError(function (arg0, arg1, arg2) { + getObject(arg0).set(getObject(arg1), arg2 >>> 0); + }, arguments) }; + imports.wbg.__wbg_buffer_5e74a88a1424a2e0 = function() { return logError(function (arg0) { + var ret = getObject(arg0).buffer; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_get_8bbb82393651dd9c = function() { return handleError(function (arg0, arg1) { + var ret = Reflect.get(getObject(arg0), getObject(arg1)); + return addHeapObject(ret); + }, arguments) }; imports.wbg.__wbg_set_c42875065132a932 = function() { return handleError(function (arg0, arg1, arg2) { var ret = Reflect.set(getObject(arg0), getObject(arg1), getObject(arg2)); + _assertBoolean(ret); return ret; }, arguments) }; imports.wbg.__wbindgen_debug_string = function(arg0, arg1) { @@ -807,18 +862,18 @@ async function init(input) { var ret = wasm.memory; return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper1345 = function(arg0, arg1, arg2) { - var ret = makeMutClosure(arg0, arg1, 563, __wbg_adapter_30); + imports.wbg.__wbindgen_closure_wrapper12230 = function() { return logError(function (arg0, arg1, arg2) { + var ret = makeClosure(arg0, arg1, 856, __wbg_adapter_30); return addHeapObject(ret); - }; - imports.wbg.__wbindgen_closure_wrapper1575 = function(arg0, arg1, arg2) { - var ret = makeClosure(arg0, arg1, 609, __wbg_adapter_33); + }, arguments) }; + imports.wbg.__wbindgen_closure_wrapper15845 = function() { return logError(function (arg0, arg1, arg2) { + var ret = makeMutClosure(arg0, arg1, 884, __wbg_adapter_33); return addHeapObject(ret); - }; - imports.wbg.__wbindgen_closure_wrapper1700 = function(arg0, arg1, arg2) { - var ret = makeMutClosure(arg0, arg1, 648, __wbg_adapter_36); + }, arguments) }; + imports.wbg.__wbindgen_closure_wrapper16308 = function() { return logError(function (arg0, arg1, arg2) { + var ret = makeMutClosure(arg0, arg1, 913, __wbg_adapter_36); return addHeapObject(ret); - }; + }, arguments) }; if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) { input = fetch(input); diff --git a/kanidmd_web_ui/pkg/kanidmd_web_ui_bg.wasm b/kanidmd_web_ui/pkg/kanidmd_web_ui_bg.wasm index f406e6d87..207b02780 100644 Binary files a/kanidmd_web_ui/pkg/kanidmd_web_ui_bg.wasm and b/kanidmd_web_ui/pkg/kanidmd_web_ui_bg.wasm differ diff --git a/kanidmd_web_ui/pkg/kanidmd_web_ui_bg.wasm.d.ts b/kanidmd_web_ui/pkg/kanidmd_web_ui_bg.wasm.d.ts new file mode 100644 index 000000000..57759cb18 --- /dev/null +++ b/kanidmd_web_ui/pkg/kanidmd_web_ui_bg.wasm.d.ts @@ -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__h79af8eb94e22cb43(a: number, b: number, c: number): void; +export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h1c17030d49aa8a7b(a: number, b: number, c: number): void; +export function _dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hb210817f7291cc2b(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; diff --git a/kanidmd_web_ui/pkg/package.json b/kanidmd_web_ui/pkg/package.json index 9bdd36443..c53aa8ba5 100644 --- a/kanidmd_web_ui/pkg/package.json +++ b/kanidmd_web_ui/pkg/package.json @@ -14,9 +14,11 @@ "files": [ "kanidmd_web_ui_bg.wasm", "kanidmd_web_ui.js", + "kanidmd_web_ui.d.ts", "LICENSE.md" ], "module": "kanidmd_web_ui.js", "homepage": "https://github.com/kanidm/kanidm/", + "types": "kanidmd_web_ui.d.ts", "sideEffects": false } \ No newline at end of file