Identity verification feature (#1819)

This commit is contained in:
Sebastiano Tocci 2023-08-16 13:02:48 +02:00 committed by GitHub
parent 87866c568b
commit 003234c2d0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 3117 additions and 372 deletions

58
Cargo.lock generated
View file

@ -2072,6 +2072,15 @@ dependencies = [
"libc",
]
[[package]]
name = "instant"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [
"cfg-if",
]
[[package]]
name = "ipnet"
version = "2.8.0"
@ -2438,6 +2447,7 @@ dependencies = [
"kanidmd_core",
"kanidmd_lib",
"oauth2",
"openssl",
"reqwest",
"serde",
"serde_json",
@ -2455,9 +2465,12 @@ name = "kanidmd_web_ui"
version = "1.1.0-rc.14-dev"
dependencies = [
"gloo",
"gloo-timers",
"js-sys",
"kanidm_proto",
"lazy_static",
"qrcode",
"regex",
"serde",
"serde-wasm-bindgen 0.5.0",
"serde_json",
@ -2467,6 +2480,7 @@ dependencies = [
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-bindgen-test",
"wasm-timer",
"web-sys",
"yew",
"yew-router",
@ -2860,7 +2874,7 @@ dependencies = [
"crossbeam-channel",
"file-id",
"notify",
"parking_lot",
"parking_lot 0.12.1",
"walkdir",
]
@ -3140,6 +3154,17 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "parking_lot"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [
"instant",
"lock_api",
"parking_lot_core 0.8.6",
]
[[package]]
name = "parking_lot"
version = "0.12.1"
@ -3147,7 +3172,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
dependencies = [
"lock_api",
"parking_lot_core",
"parking_lot_core 0.9.8",
]
[[package]]
name = "parking_lot_core"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
dependencies = [
"cfg-if",
"instant",
"libc",
"redox_syscall 0.2.16",
"smallvec",
"winapi",
]
[[package]]
@ -4929,6 +4968,21 @@ dependencies = [
"quote",
]
[[package]]
name = "wasm-timer"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f"
dependencies = [
"futures",
"js-sys",
"parking_lot 0.11.2",
"pin-utils",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "web-sys"
version = "0.3.64"

View file

@ -1,7 +1,8 @@
use std::collections::BTreeMap;
use kanidm_proto::v1::{
AccountUnixExtend, CredentialStatus, Entry, SingleStringRequest, UatStatus,
use kanidm_proto::{
internal::{IdentifyUserRequest, IdentifyUserResponse},
v1::{AccountUnixExtend, CredentialStatus, Entry, SingleStringRequest, UatStatus},
};
use uuid::Uuid;
@ -214,6 +215,18 @@ impl KanidmClient {
.await
}
pub async fn idm_person_identify_user(
&self,
id: &str,
request: IdentifyUserRequest,
) -> Result<IdentifyUserResponse, ClientError> {
self.perform_post_request(
["/v1/person/", id, "/_identify_user"].concat().as_str(),
request,
)
.await
}
pub async fn idm_account_radius_credential_get(
&self,
id: &str,

View file

@ -26,3 +26,22 @@ pub struct ScimSyncToken {
#[serde(default)]
pub purpose: ApiTokenPurpose,
}
// State machine states and transitions for the identity verification system feature!
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum IdentifyUserRequest {
Start,
SubmitCode { other_totp: u32 },
DisplayCode,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum IdentifyUserResponse {
IdentityVerificationUnavailable,
IdentityVerificationAvailable,
ProvideCode { step: u32, totp: u32 },
WaitForCode,
Success,
CodeFailure,
InvalidUserId,
}

View file

@ -4,12 +4,15 @@ use std::net::IpAddr;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use kanidm_proto::internal::AppLink;
use kanidm_proto::internal::{AppLink, IdentifyUserRequest, IdentifyUserResponse};
use kanidm_proto::v1::{
ApiToken, AuthIssueSession, AuthRequest, BackupCodesView, CURequest, CUSessionToken, CUStatus,
CredentialStatus, Entry as ProtoEntry, OperationError, RadiusAuthToken, SearchRequest,
SearchResponse, UatStatus, UnixGroupToken, UnixUserToken, UserAuthToken, WhoamiResponse,
};
use kanidmd_lib::idm::identityverification::{
IdentifyUserDisplayCodeEvent, IdentifyUserStartEvent, IdentifyUserSubmitCodeEvent,
};
use ldap3_proto::simple::*;
use regex::Regex;
use tracing::{error, info, instrument, trace};
@ -883,6 +886,47 @@ impl QueryServerReadV1 {
idms_prox_read.account_list_user_auth_tokens(&lte)
}
#[instrument(
level = "info",
skip_all,
fields(uuid = ?eventid)
)]
pub async fn handle_user_identity_verification(
&self,
uat: Option<String>,
eventid: Uuid,
user_request: IdentifyUserRequest,
other_id: String,
) -> Result<IdentifyUserResponse, OperationError> {
trace!("{:?}", &user_request);
let ct = duration_from_epoch_now();
let mut idms_prox_read = self.idms.proxy_read().await;
let ident = idms_prox_read
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
.map_err(|e| {
admin_error!("Invalid identity: {:?}", e);
e
})?;
let target = idms_prox_read
.qs_read
.name_to_uuid(&other_id)
.map_err(|e| {
admin_error!("No user found with the provided ID: {:?}", e);
e
})?;
match user_request {
IdentifyUserRequest::Start => idms_prox_read
.handle_identify_user_start(&IdentifyUserStartEvent::new(target, ident)),
IdentifyUserRequest::DisplayCode => idms_prox_read.handle_identify_user_display_code(
&IdentifyUserDisplayCodeEvent::new(target, ident),
),
IdentifyUserRequest::SubmitCode { other_totp } => idms_prox_read
.handle_identify_user_submit_code(&IdentifyUserSubmitCodeEvent::new(
target, ident, other_totp,
)),
}
}
#[instrument(
level = "info",
skip_all,

View file

@ -4,26 +4,26 @@ use axum::extract::{Path, Query, State};
use axum::headers::{CacheControl, HeaderMapExt};
use axum::middleware::from_fn;
use axum::response::{IntoResponse, Response};
use axum::routing::{delete, get, post, put};
use axum::{Extension, Json, Router};
use axum_macros::debug_handler;
use compact_jwt::Jws;
use http::{HeaderMap, HeaderValue, StatusCode};
use hyper::Body;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use kanidm_proto::internal::IdentifyUserRequest;
use kanidm_proto::v1::{
AccountUnixExtend, ApiTokenGenerate, AuthIssueSession, AuthRequest, AuthResponse,
AuthState as ProtoAuthState, CUIntentToken, CURequest, CUSessionToken, CreateRequest,
DeleteRequest, Entry as ProtoEntry, GroupUnixExtend, ModifyRequest, SearchRequest,
SingleStringRequest,
};
use kanidmd_lib::idm::event::AuthResult;
use kanidmd_lib::idm::AuthState;
use kanidmd_lib::prelude::*;
use kanidmd_lib::value::PartialValue;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::https::extractors::TrustedClientIp;
use crate::https::to_axum_response;
@ -995,6 +995,19 @@ pub async fn account_delete_id_unix_credential(
to_axum_response(res)
}
pub async fn person_post_identify_user(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
Path(id): Path<String>,
Json(user_request): Json<IdentifyUserRequest>,
) -> impl IntoResponse {
let res = state
.qe_r_ref
.handle_user_identity_verification(kopid.uat, kopid.eventid, user_request, id)
.await;
to_axum_response(res)
}
pub async fn group_get(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
@ -1519,6 +1532,10 @@ pub fn router(state: ServerState) -> Router<ServerState> {
"/v1/person/:id/_unix/_credential",
put(account_put_id_unix_credential).delete(account_delete_id_unix_credential),
)
.route(
"/v1/person/:id/_identify_user",
post(person_post_identify_user),
)
// Service accounts
.route(
"/v1/service_account",

View file

@ -611,6 +611,8 @@ pub enum DbValueSetV2 {
ApiToken(Vec<DbValueApiToken>),
#[serde(rename = "SA")]
AuditLogString(Vec<(Cid, String)>),
#[serde(rename = "EK")]
EcKeyPrivate(Vec<u8>),
}
impl DbValueSetV2 {
@ -654,6 +656,8 @@ impl DbValueSetV2 {
DbValueSetV2::UiHint(set) => set.len(),
DbValueSetV2::TotpSecret(set) => set.len(),
DbValueSetV2::AuditLogString(set) => set.len(),
DbValueSetV2::EcKeyPrivate(_key) => 1, // here we have to hard code it because the Vec<u8>
// represents the bytes of SINGLE(!) key
}
}

View file

@ -644,6 +644,7 @@ lazy_static! {
("acp_modify_removedattr", Value::new_iutf8("devicekeys")),
("acp_modify_removedattr", Value::new_iutf8("api_token_session")),
("acp_modify_removedattr", Value::new_iutf8("user_auth_token_session")),
("acp_modify_removedattr", Value::new_iutf8("id_verification_eckey")),
("acp_modify_presentattr", Value::new_iutf8("name")),
("acp_modify_presentattr", Value::new_iutf8("displayname")),
@ -857,6 +858,7 @@ lazy_static! {
("acp_modify_removedattr", Value::new_iutf8("devicekeys")),
("acp_modify_removedattr", Value::new_iutf8("api_token_session")),
("acp_modify_removedattr", Value::new_iutf8("user_auth_token_session")),
("acp_modify_removedattr", Value::new_iutf8("id_verification_eckey")),
("acp_modify_presentattr", Value::new_iutf8("name")),
("acp_modify_presentattr", Value::new_iutf8("displayname")),

View file

@ -44,6 +44,18 @@ pub static ref SCHEMA_ATTR_MAIL: SchemaAttribute = SchemaAttribute {
..Default::default()
};
pub static ref SCHEMA_ATTR_EC_KEY_PRIVATE: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_EC_KEY_PRIVATE,
name: "id_verification_eckey".into(),
description: "Account verification private key.".to_string(),
index: vec![IndexType::Presence],
unique: false,
sync_allowed: false,
syntax: SyntaxType::EcKeyPrivate,
..Default::default()
};
pub static ref SCHEMA_ATTR_SSH_PUBLICKEY: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_SSH_PUBLICKEY,
name: "ssh_publickey".into(),
@ -537,7 +549,7 @@ pub static ref SCHEMA_CLASS_PERSON: SchemaClass = SchemaClass {
description: "Object representation of a person".to_string(),
sync_allowed: true,
systemmay: attrstring_vec!(["mail", "legalname"]),
systemmay: attrstring_vec!(["mail", "legalname", "id_verification_eckey"]),
systemmust: attrstring_vec!(["displayname", "name"]),
..Default::default()
};

View file

@ -224,6 +224,7 @@ pub const UUID_SCHEMA_ATTR_PRIVATE_COOKIE_KEY: Uuid = uuid!("00000000-0000-0000-
pub const UUID_SCHEMA_ATTR_DOMAIN_LDAP_BASEDN: Uuid = uuid!("00000000-0000-0000-0000-ffff00000131");
pub const UUID_SCHEMA_ATTR_DYNMEMBER: Uuid = uuid!("00000000-0000-0000-0000-ffff00000132");
pub const UUID_SCHEMA_ATTR_NAME_HISTORY: Uuid = uuid!("00000000-0000-0000-0000-ffff00000133");
pub const UUID_SCHEMA_ATTR_EC_KEY_PRIVATE: Uuid = uuid!("00000000-0000-0000-0000-ffff00000134");
pub const UUID_SCHEMA_ATTR_SYNC_CREDENTIAL_PORTAL: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000136");

View file

@ -36,6 +36,8 @@ use kanidm_proto::v1::{
UiHint,
};
use ldap3_proto::simple::{LdapPartialAttribute, LdapSearchResultEntry};
use openssl::ec::EcKey;
use openssl::pkey::{Private, Public};
use smartstring::alias::String as AttrString;
use time::OffsetDateTime;
use tracing::trace;
@ -2635,6 +2637,17 @@ impl<VALID, STATE> Entry<VALID, STATE> {
.and_then(|vs| vs.to_jws_key_es256_single())
}
pub fn get_ava_single_eckey_private(&self, attr: &str) -> Option<&EcKey<Private>> {
self.attrs
.get(attr)
.and_then(|vs| vs.to_eckey_private_single())
}
pub fn get_ava_single_eckey_public(&self, attr: &str) -> Option<&EcKey<Public>> {
self.attrs
.get(attr)
.and_then(|vs| vs.to_eckey_public_single())
}
#[inline(always)]
/// Return a single security principle name, if valid to transform this value.
pub(crate) fn generate_spn(&self, domain_name: &str) -> Option<Value> {

View file

@ -0,0 +1,650 @@
use std::time::SystemTime;
use kanidm_proto::{internal::IdentifyUserResponse, v1::OperationError};
use openssl::ec::EcKey;
use openssl::pkey::{PKey, Private, Public};
use openssl::pkey_ctx::PkeyCtx;
use sketching::admin_error;
use uuid::Uuid;
use crate::credential::totp::{Totp, TotpAlgo, TotpDigits};
use crate::prelude::{tagged_event, EventTag};
use crate::server::QueryServerTransaction;
use crate::{event::SearchEvent, server::identity::Identity};
use crate::idm::server::IdmServerProxyReadTransaction;
static TOTP_STEP: u64 = 30;
#[derive(Debug)]
pub struct IdentifyUserStartEvent {
pub target: Uuid,
pub ident: Identity,
}
impl IdentifyUserStartEvent {
pub fn new(target: Uuid, ident: Identity) -> Self {
IdentifyUserStartEvent { target, ident }
}
}
pub struct IdentifyUserDisplayCodeEvent {
pub target: Uuid,
pub ident: Identity,
}
impl IdentifyUserDisplayCodeEvent {
pub fn new(target: Uuid, ident: Identity) -> Self {
IdentifyUserDisplayCodeEvent { target, ident }
}
}
pub struct IdentifyUserSubmitCodeEvent {
pub code: u32,
pub target: Uuid,
pub ident: Identity,
}
impl IdentifyUserSubmitCodeEvent {
pub fn new(target: Uuid, ident: Identity, code: u32) -> Self {
IdentifyUserSubmitCodeEvent {
target,
ident,
code,
}
}
}
impl<'a> IdmServerProxyReadTransaction<'a> {
pub fn handle_identify_user_start(
&mut self,
IdentifyUserStartEvent { target, ident }: &IdentifyUserStartEvent,
) -> Result<IdentifyUserResponse, OperationError> {
if let Some(early_response) = self.check_for_early_return_conditions(ident, target)? {
return Ok(early_response);
}
let response = if ident.get_uuid() < Some(*target) {
IdentifyUserResponse::WaitForCode
} else {
let totp_secret = self.get_self_totp_secret(target, ident)?;
let totp = self.compute_totp(totp_secret)?;
IdentifyUserResponse::ProvideCode {
step: TOTP_STEP as u32,
totp,
}
};
Ok(response)
}
pub fn handle_identify_user_display_code(
&mut self,
IdentifyUserDisplayCodeEvent { target, ident }: &IdentifyUserDisplayCodeEvent,
) -> Result<IdentifyUserResponse, OperationError> {
if let Some(early_response) = self.check_for_early_return_conditions(ident, target)? {
return Ok(early_response);
}
let totp_secret = self.get_self_totp_secret(target, ident)?;
let totp = self.compute_totp(totp_secret)?;
Ok(IdentifyUserResponse::ProvideCode {
step: TOTP_STEP as u32,
totp,
})
}
pub fn handle_identify_user_submit_code(
&mut self,
IdentifyUserSubmitCodeEvent {
target,
ident,
code,
}: &IdentifyUserSubmitCodeEvent,
) -> Result<IdentifyUserResponse, OperationError> {
if let Some(early_response) = self.check_for_early_return_conditions(ident, target)? {
return Ok(early_response);
}
let totp_secret = self.get_other_user_totp_secret(target, ident)?;
let other_user_totp = self.compute_totp(totp_secret)?;
if other_user_totp != *code {
return Ok(IdentifyUserResponse::CodeFailure);
}
// if we are the first it means now it's time to go for ProvideCode, otherwise we just confirm that the code is correct
// (we know this for a fact as we have already checked that the code is correct)
let res = if ident.get_uuid() < Some(*target) {
let shared_secret = self.get_self_totp_secret(target, ident)?;
let totp = self.compute_totp(shared_secret)?;
IdentifyUserResponse::ProvideCode {
step: TOTP_STEP as u32,
totp,
}
} else {
IdentifyUserResponse::Success
};
Ok(res)
}
// End of public functions
fn check_for_early_return_conditions(
&mut self,
ident: &Identity,
target: &Uuid,
) -> Result<Option<IdentifyUserResponse>, OperationError> {
// here we check that the identify user feature is available before we do anything else
if !self.check_if_identify_feature_available(ident)? {
return Ok(Some(IdentifyUserResponse::IdentityVerificationUnavailable));
};
if !self.is_valid_user_uuid(ident, target)? {
return Ok(Some(IdentifyUserResponse::InvalidUserId));
};
// here we check if the user provided their own uuid, if they did we just respond with IdentityVerificationAvailable.
if ident.get_uuid().eq(&Some(*target)) {
return Ok(Some(IdentifyUserResponse::IdentityVerificationAvailable));
};
Ok(None)
}
fn check_if_identify_feature_available(
&mut self,
ident: &Identity,
) -> Result<bool, OperationError> {
let search = match SearchEvent::from_whoami_request(ident.clone(), &self.qs_read) {
Ok(s) => s,
Err(e) => {
admin_error!("Failed to generate whoami search event: {:?}", e);
return Err(e);
}
};
self.qs_read
.search(&search)
.and_then(|mut entries| entries.pop().ok_or(OperationError::NoMatchingEntries))
.map(
|entry| match entry.get_ava_single_eckey_private("id_verification_eckey") {
Some(key) => key.check_key().is_ok(),
None => false,
},
)
}
fn is_valid_user_uuid(
&mut self,
ident: &Identity,
target: &Uuid,
) -> Result<bool, OperationError> {
let search =
match SearchEvent::from_target_uuid_request(ident.clone(), *target, &self.qs_read) {
Ok(s) => s,
Err(e) => {
admin_error!("Failed to retrieve user with the given UUID: {:?}", e);
return Err(e);
}
};
let user_entry = self
.qs_read
.search(&search)
.and_then(|mut entries| entries.pop().ok_or(OperationError::NoMatchingEntries))?;
match user_entry.get_ava_single_eckey_public("id_verification_eckey") {
Some(key) => Ok(key.check_key().is_ok()),
None => Ok(false),
}
}
fn get_user_own_key(&mut self, ident: &Identity) -> Result<EcKey<Private>, OperationError> {
let search = match SearchEvent::from_whoami_request(ident.clone(), &self.qs_read) {
Ok(s) => s,
Err(e) => {
admin_error!(
"Failed to retrieve user with the given UUID: {}. \n{:?}",
ident.get_uuid().unwrap_or_default(),
e
);
return Err(e);
}
};
self.qs_read
.search(&search)
.and_then(|mut entries| entries.pop().ok_or(OperationError::NoMatchingEntries))
.and_then(
|entry| match entry.get_ava_single_eckey_private("id_verification_eckey") {
Some(key) => Ok(key.clone()),
None => Err(OperationError::InvalidAccountState(format!(
"{}'s private key is missing!",
ident.get_uuid().unwrap_or_default()
))),
},
)
}
fn get_other_user_public_key(
&mut self,
target: &Uuid,
ident: &Identity,
) -> Result<EcKey<Public>, OperationError> {
let search =
match SearchEvent::from_target_uuid_request(ident.clone(), *target, &self.qs_read) {
Ok(s) => s,
Err(e) => {
admin_error!(
"Failed to retrieve user with the given UUID: {}. \n{:?}",
ident.get_uuid().unwrap_or_default(),
e
);
return Err(e);
}
};
self.qs_read
.search(&search)
.and_then(|mut entries| entries.pop().ok_or(OperationError::NoMatchingEntries))
.and_then(
|entry| match entry.get_ava_single_eckey_public("id_verification_eckey") {
Some(key) => Ok(key.clone()),
None => Err(OperationError::InvalidAccountState(format!(
"{target}'s public key is missing!",
))),
},
)
}
fn compute_totp(&mut self, totp_secret: Vec<u8>) -> Result<u32, OperationError> {
let totp = Totp::new(totp_secret, TOTP_STEP, TotpAlgo::Sha256, TotpDigits::Six);
let current_time = SystemTime::now();
totp.do_totp(&current_time)
.map_err(|_| OperationError::CryptographyError)
}
fn get_self_totp_secret(
&mut self,
target: &Uuid,
ident: &Identity,
) -> Result<Vec<u8>, OperationError> {
let self_private = self.get_user_own_key(ident)?;
let other_user_public_key = self.get_other_user_public_key(target, ident)?;
let mut shared_key = self.derive_shared_key(self_private, other_user_public_key)?;
let Some(self_uuid) = ident.get_uuid() else {
return Err(OperationError::NotAuthenticated)
};
shared_key.extend_from_slice(self_uuid.as_bytes());
Ok(shared_key)
}
fn get_other_user_totp_secret(
&mut self,
target: &Uuid,
ident: &Identity,
) -> Result<Vec<u8>, OperationError> {
let self_private = self.get_user_own_key(ident)?;
let other_user_public_key = self.get_other_user_public_key(target, ident)?;
let mut shared_key = self.derive_shared_key(self_private, other_user_public_key)?;
shared_key.extend_from_slice(target.as_bytes());
Ok(shared_key)
}
fn derive_shared_key(
&self,
private: EcKey<Private>,
public: EcKey<Public>,
) -> Result<Vec<u8>, OperationError> {
let cryptography_error = |_| OperationError::CryptographyError;
let pkey_private = PKey::from_ec_key(private).map_err(cryptography_error)?;
let pkey_public = PKey::from_ec_key(public).map_err(cryptography_error)?;
let mut private_key_ctx: PkeyCtx<Private> =
PkeyCtx::new(&pkey_private).map_err(cryptography_error)?;
private_key_ctx.derive_init().map_err(cryptography_error)?;
private_key_ctx
.derive_set_peer(&pkey_public)
.map_err(cryptography_error)?;
let keylen = private_key_ctx.derive(None).map_err(cryptography_error)?;
let mut tmp_vec = vec![0; keylen];
let buffer = tmp_vec.as_mut_slice();
private_key_ctx
.derive(Some(buffer))
.map_err(cryptography_error)?;
Ok(buffer.to_vec())
}
}
#[cfg(test)]
mod test {
use kanidm_proto::internal::IdentifyUserResponse;
use crate::idm::identityverification::{
IdentifyUserDisplayCodeEvent, IdentifyUserStartEvent, IdentifyUserSubmitCodeEvent,
};
use crate::prelude::*;
#[idm_test]
async fn test_identity_verification_unavailable(
idms: &IdmServer,
_idms_delayed: &IdmServerDelayed,
) {
let ct = duration_from_epoch_now();
let mut idms_prox_write = idms.proxy_write(ct).await;
let self_uuid = Uuid::new_v4();
let valid_user_uuid = Uuid::new_v4();
let e1 = create_invalid_user_account(self_uuid);
let e2 = create_valid_user_account(valid_user_uuid);
let ce = CreateEvent::new_internal(vec![e1, e2]);
assert!(idms_prox_write.qs_write.create(&ce).is_ok());
assert!(idms_prox_write.commit().is_ok());
let mut idms_prox_read = idms.proxy_read().await;
let ident = idms_prox_read
.qs_read
.internal_search_uuid(self_uuid)
.map(Identity::from_impersonate_entry_readonly)
.expect("Failed to impersonate identity");
let res = idms_prox_read
.handle_identify_user_start(&IdentifyUserStartEvent::new(self_uuid, ident.clone()));
assert!(matches!(
res,
Ok(IdentifyUserResponse::IdentityVerificationUnavailable)
));
let res = idms_prox_read.handle_identify_user_start(&IdentifyUserStartEvent::new(
valid_user_uuid,
ident.clone(),
));
assert!(matches!(
res,
Ok(IdentifyUserResponse::IdentityVerificationUnavailable)
));
let res = idms_prox_read.handle_identify_user_display_code(
&IdentifyUserDisplayCodeEvent::new(valid_user_uuid, ident.clone()),
);
assert!(matches!(
res,
Ok(IdentifyUserResponse::IdentityVerificationUnavailable)
));
let res = idms_prox_read.handle_identify_user_submit_code(
&IdentifyUserSubmitCodeEvent::new(valid_user_uuid, ident, 123456),
);
assert!(matches!(
res,
Ok(IdentifyUserResponse::IdentityVerificationUnavailable)
));
}
#[idm_test]
async fn test_invalid_user_id(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
let ct = duration_from_epoch_now();
let mut idms_prox_write = idms.proxy_write(ct).await;
let invalid_user_uuid = Uuid::new_v4();
let valid_user_a_uuid = Uuid::new_v4();
let valid_user_b_uuid = Uuid::new_v4();
let e1 = create_invalid_user_account(invalid_user_uuid);
let e2 = create_valid_user_account(valid_user_a_uuid);
let e3 = create_valid_user_account(valid_user_b_uuid);
let ce = CreateEvent::new_internal(vec![e1, e2, e3]);
assert!(idms_prox_write.qs_write.create(&ce).is_ok());
assert!(idms_prox_write.commit().is_ok());
let mut idms_prox_read = idms.proxy_read().await;
let ident = idms_prox_read
.qs_read
.internal_search_uuid(valid_user_a_uuid)
.map(Identity::from_impersonate_entry_readonly)
.expect("Failed to impersonate identity");
let res = idms_prox_read.handle_identify_user_start(&IdentifyUserStartEvent::new(
invalid_user_uuid,
ident.clone(),
));
assert!(matches!(res, Ok(IdentifyUserResponse::InvalidUserId)));
let res = idms_prox_read.handle_identify_user_start(&IdentifyUserStartEvent::new(
invalid_user_uuid,
ident.clone(),
));
assert!(matches!(res, Ok(IdentifyUserResponse::InvalidUserId)));
let res = idms_prox_read.handle_identify_user_display_code(
&IdentifyUserDisplayCodeEvent::new(invalid_user_uuid, ident.clone()),
);
assert!(matches!(res, Ok(IdentifyUserResponse::InvalidUserId)));
let res = idms_prox_read.handle_identify_user_submit_code(
&IdentifyUserSubmitCodeEvent::new(invalid_user_uuid, ident, 123456),
);
assert!(matches!(res, Ok(IdentifyUserResponse::InvalidUserId)));
}
#[idm_test]
async fn test_start_event(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
let ct = duration_from_epoch_now();
let mut idms_prox_write = idms.proxy_write(ct).await;
let valid_user_a_uuid = Uuid::new_v4();
let e = create_valid_user_account(valid_user_a_uuid);
let ce = CreateEvent::new_internal(vec![e]);
assert!(idms_prox_write.qs_write.create(&ce).is_ok());
assert!(idms_prox_write.commit().is_ok());
let mut idms_prox_read = idms.proxy_read().await;
let ident = idms_prox_read
.qs_read
.internal_search_uuid(valid_user_a_uuid)
.map(Identity::from_impersonate_entry_readonly)
.expect("Failed to impersonate identity");
let res = idms_prox_read.handle_identify_user_start(&IdentifyUserStartEvent::new(
valid_user_a_uuid,
ident.clone(),
));
assert!(matches!(
res,
Ok(IdentifyUserResponse::IdentityVerificationAvailable)
));
}
#[idm_test] // actually this is somewhat a duplicate of `test_full_identification_flow` inside the testkit, with the exception that this
//tests ONLY the totp code correctness and not the flow correctness. To test the correctness it obviously needs to also
// enforce some flow checks, but this is not the primary scope of this test
async fn test_code_correctness(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
let ct = duration_from_epoch_now();
let mut idms_prox_write = idms.proxy_write(ct).await;
let user_a_uuid = Uuid::new_v4();
let user_b_uuid = Uuid::new_v4();
let e1 = create_valid_user_account(user_a_uuid);
let e2 = create_valid_user_account(user_b_uuid);
let ce = CreateEvent::new_internal(vec![e1, e2]);
assert!(idms_prox_write.qs_write.create(&ce).is_ok());
assert!(idms_prox_write.commit().is_ok());
let mut idms_prox_read = idms.proxy_read().await;
let ident_a = idms_prox_read
.qs_read
.internal_search_uuid(user_a_uuid)
.map(Identity::from_impersonate_entry_readonly)
.expect("Failed to impersonate identity");
let ident_b = idms_prox_read
.qs_read
.internal_search_uuid(user_b_uuid)
.map(Identity::from_impersonate_entry_readonly)
.expect("Failed to impersonate identity");
let (lower_user, lower_user_uuid, higher_user, higher_user_uuid) =
if user_a_uuid < user_b_uuid {
(ident_a, user_a_uuid, ident_b, user_b_uuid)
} else {
(ident_b, user_b_uuid, ident_a, user_a_uuid)
};
// First the user with the lowest uuid receives the uuid from the other user
let res_higher_user = idms_prox_read.handle_identify_user_start(
&IdentifyUserStartEvent::new(lower_user_uuid, higher_user.clone()),
);
let Ok(IdentifyUserResponse::ProvideCode { totp, .. }) = res_higher_user else {
return assert!(false);
};
let res_lower_user_wrong = idms_prox_read.handle_identify_user_submit_code(
&IdentifyUserSubmitCodeEvent::new(higher_user_uuid, lower_user.clone(), totp + 1),
);
assert!(matches!(
res_lower_user_wrong,
Ok(IdentifyUserResponse::CodeFailure)
));
let res_lower_user_correct = idms_prox_read.handle_identify_user_submit_code(
&IdentifyUserSubmitCodeEvent::new(higher_user_uuid, lower_user.clone(), totp),
);
assert!(matches!(
res_lower_user_correct,
Ok(IdentifyUserResponse::ProvideCode { .. })
));
// now we need to get the code from the lower_user and submit it to the higher_user
let Ok(IdentifyUserResponse::ProvideCode{totp, ..}) = res_lower_user_correct else {
return assert!(false);
};
let res_higher_user_2_wrong = idms_prox_read.handle_identify_user_submit_code(
&IdentifyUserSubmitCodeEvent::new(lower_user_uuid, higher_user.clone(), totp + 1),
);
assert!(matches!(
res_higher_user_2_wrong,
Ok(IdentifyUserResponse::CodeFailure)
));
let res_higher_user_2_correct = idms_prox_read.handle_identify_user_submit_code(
&IdentifyUserSubmitCodeEvent::new(lower_user_uuid, higher_user.clone(), totp),
);
assert!(matches!(
res_higher_user_2_correct,
Ok(IdentifyUserResponse::Success)
));
}
#[idm_test]
async fn test_totps_differ(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
let ct = duration_from_epoch_now();
let mut idms_prox_write = idms.proxy_write(ct).await;
let user_a_uuid = Uuid::new_v4();
let user_b_uuid = Uuid::new_v4();
let e1 = create_valid_user_account(user_a_uuid);
let e2 = create_valid_user_account(user_b_uuid);
let ce = CreateEvent::new_internal(vec![e1, e2]);
assert!(idms_prox_write.qs_write.create(&ce).is_ok());
assert!(idms_prox_write.commit().is_ok());
let mut idms_prox_read = idms.proxy_read().await;
let ident_a = idms_prox_read
.qs_read
.internal_search_uuid(user_a_uuid)
.map(Identity::from_impersonate_entry_readonly)
.expect("Failed to impersonate identity");
let ident_b = idms_prox_read
.qs_read
.internal_search_uuid(user_b_uuid)
.map(Identity::from_impersonate_entry_readonly)
.expect("Failed to impersonate identity");
let (lower_user, lower_user_uuid, higher_user, higher_user_uuid) =
if user_a_uuid < user_b_uuid {
(ident_a, user_a_uuid, ident_b, user_b_uuid)
} else {
(ident_b, user_b_uuid, ident_a, user_a_uuid)
};
// First twe retrieve the higher user code
let res_higher_user = idms_prox_read.handle_identify_user_start(
&IdentifyUserStartEvent::new(lower_user_uuid, higher_user.clone()),
);
let Ok(IdentifyUserResponse::ProvideCode { totp: higher_user_totp, .. }) = res_higher_user else {
return assert!(false);
};
// then we get the lower user code
let res_lower_user_correct =
idms_prox_read.handle_identify_user_submit_code(&IdentifyUserSubmitCodeEvent::new(
higher_user_uuid,
lower_user.clone(),
higher_user_totp,
));
if let Ok(IdentifyUserResponse::ProvideCode {
totp: lower_user_totp,
..
}) = res_lower_user_correct
{
assert_ne!(higher_user_totp, lower_user_totp);
} else {
assert!(false);
}
}
fn create_valid_user_account(uuid: Uuid) -> EntryInitNew {
let mut name = String::from("valid_user");
name.push_str(&uuid.to_string());
// if anyone from the future will see this test failing because of a schema violation
// and wonders to this line of code I'm sorry to have wasted your time
name.truncate(14);
entry_init!(
("class", Value::new_class("object")),
("class", Value::new_class("account")),
("class", Value::new_class("person")),
("name", Value::new_iname(&name)),
("uuid", Value::Uuid(uuid)),
("description", Value::new_utf8s("some valid user")),
("displayname", Value::new_utf8s("Some valid user"))
)
}
fn create_invalid_user_account(uuid: Uuid) -> EntryInitNew {
entry_init!(
("class", Value::new_class("object")),
("class", Value::new_class("account")),
("class", Value::new_class("service_account")),
("name", Value::new_iname("invalid_user")),
("uuid", Value::Uuid(uuid)),
("description", Value::new_utf8s("invalid_user")),
("displayname", Value::new_utf8s("Invalid user"))
)
}
}

View file

@ -11,6 +11,7 @@ pub mod credupdatesession;
pub mod delayed;
pub mod event;
pub mod group;
pub mod identityverification;
pub mod ldap;
pub mod oauth2;
pub mod radius;

View file

@ -0,0 +1,284 @@
use openssl::ec::{EcGroup, EcKey};
use openssl::nid::Nid;
use sketching::{admin_error, security_info};
use uuid::Uuid;
use super::Plugin;
use crate::event::{CreateEvent, ModifyEvent};
use crate::modify::{ModifyList, ModifyValid};
use crate::prelude::{BatchModifyEvent, EntryInvalidCommitted, Modify};
use crate::prelude::{Entry, EntryInvalid, EntryInvalidNew, OperationError};
use crate::server::QueryServerWriteTransaction;
use crate::value::PartialValue;
use sketching::tagged_event;
use sketching::EventTag;
lazy_static! {
// it contains all the partialvalues used to match against an Entry's class,
// we need ALL partialvalues to match in order to target the entry
static ref CLASSES_TO_UPDATE: [PartialValue; 3] = [PartialValue::new_iutf8("account"), PartialValue::new_iutf8("person"), PartialValue::new_iutf8("object")];
static ref DEFAULT_KEY_GROUP: EcGroup = {
let nid = Nid::X9_62_PRIME256V1; // NIST P-256 curve
#[allow(clippy::unwrap_used)]
EcGroup::from_curve_name(nid).unwrap()
};
}
pub struct EcdhKeyGen {}
impl EcdhKeyGen {
fn is_entry_to_update<VALUE, STATE>(entry: &mut Entry<VALUE, STATE>) -> bool {
CLASSES_TO_UPDATE
.iter()
.all(|pv| entry.attribute_equality("class", pv))
}
// we optionally provide a target_cand to update only the entry with the given uuid
fn generate_key<STATE: Clone>(
cands: &mut [Entry<EntryInvalid, STATE>],
target_cand: Option<Uuid>,
) -> Result<(), OperationError> {
for cand in cands.iter_mut() {
if Self::is_entry_to_update(cand) {
if let (Some(target_cand), Some(current_uuid)) = (target_cand, cand.get_uuid()) {
if target_cand != current_uuid {
continue;
}
}
let new_private_key = EcKey::generate(&DEFAULT_KEY_GROUP).map_err(|e| {
admin_error!(err = ?e, "Unable to generate identification ECDH private key");
OperationError::CryptographyError
})?;
cand.add_ava_if_not_exist(
"id_verification_eckey",
crate::value::Value::EcKeyPrivate(new_private_key),
)
}
}
Ok(())
}
fn handle_modify(
cands: &mut [EntryInvalidCommitted],
me: &ModifyEvent,
) -> Result<(), OperationError> {
if Self::should_regenerate_ecdh_key(&me.modlist)? {
security_info!("regenerating personal ecdh secret");
Self::generate_key(cands, None)?;
};
Ok(())
}
fn handle_batch_modify(
cands: &mut [EntryInvalidCommitted],
me: &BatchModifyEvent,
) -> Result<(), OperationError> {
for (uuid, modlist) in me.modset.iter() {
if Self::should_regenerate_ecdh_key(modlist)? {
security_info!("regenerating personal ecdh secret");
Self::generate_key(cands, Some(*uuid))?;
};
}
Ok(())
}
fn should_regenerate_ecdh_key(
modlist: &ModifyList<ModifyValid>,
) -> Result<bool, OperationError> {
let modify_present_attempted = modlist.iter().any(|m| match m {
Modify::Present(a, _) => a == "id_verification_eckey",
_ => false,
});
if modify_present_attempted {
Err(OperationError::SystemProtectedAttribute)
} else {
let should_regenerate_ecdh_key = modlist.iter().any(|m| match m {
Modify::Purged(a) | Modify::Removed(a, _) => a == "id_verification_eckey",
_ => false,
});
Ok(should_regenerate_ecdh_key)
}
}
}
impl Plugin for EcdhKeyGen {
fn id() -> &'static str {
"plugin_ecdhkey_gen"
}
fn pre_create_transform(
_qs: &mut QueryServerWriteTransaction,
cand: &mut Vec<EntryInvalidNew>,
_ce: &CreateEvent,
) -> Result<(), OperationError> {
Self::generate_key(cand, None)
}
fn pre_modify(
_qs: &mut crate::server::QueryServerWriteTransaction,
_pre_cand: &[std::sync::Arc<crate::prelude::EntrySealedCommitted>],
cand: &mut Vec<crate::prelude::EntryInvalidCommitted>,
me: &crate::event::ModifyEvent,
) -> Result<(), kanidm_proto::v1::OperationError> {
Self::handle_modify(cand, me)
}
fn pre_batch_modify(
_qs: &mut crate::server::QueryServerWriteTransaction,
_pre_cand: &[std::sync::Arc<crate::prelude::EntrySealedCommitted>],
cand: &mut Vec<crate::prelude::EntryInvalidCommitted>,
me: &crate::server::batch_modify::BatchModifyEvent,
) -> Result<(), kanidm_proto::v1::OperationError> {
Self::handle_batch_modify(cand, me)
}
}
#[cfg(test)]
mod tests {
use openssl::ec::EcKey;
use uuid::Uuid;
use crate::plugins::eckeygen::DEFAULT_KEY_GROUP;
use crate::prelude::{Entry, EntryInit, EntryNew};
use crate::value::Value;
use crate::valueset;
#[test]
fn test_new_user_generate_key() {
let uuid = Uuid::new_v4();
let ea = entry_init!(
("class", Value::new_class("account")),
("class", Value::new_class("person")),
("class", Value::new_class("object")),
("name", Value::new_iname("test_name")),
("uuid", Value::Uuid(uuid)),
("description", Value::new_utf8s("testperson")),
("displayname", Value::new_utf8s("Test Person"))
);
let preload: Vec<Entry<EntryInit, EntryNew>> = Vec::new();
let create = vec![ea];
run_create_test!(
Ok(()),
preload,
create,
None,
|qs: &mut QueryServerWriteTransaction| {
let e = qs.internal_search_uuid(uuid).expect("failed to get entry");
let key = e
.get_ava_single_eckey_private("id_verification_eckey")
.expect("unable to retrieve the ecdh key");
assert!(key.check_key().is_ok())
}
);
}
#[test]
fn test_modify_present_ecdkey() {
let ea = entry_init!(
("class", Value::new_class("account")),
("class", Value::new_class("person")),
("class", Value::new_class("object")),
("name", Value::new_iname("test_name")),
("description", Value::new_utf8s("testperson")),
("displayname", Value::new_utf8s("Test person!"))
);
let preload = vec![ea];
let new_private_key = EcKey::generate(&DEFAULT_KEY_GROUP).unwrap();
run_modify_test!(
Err(OperationError::SystemProtectedAttribute),
preload,
filter!(f_eq("name", PartialValue::new_iname("test_name"))),
modlist!([m_pres(
"id_verification_eckey",
&Value::EcKeyPrivate(new_private_key)
)]),
None,
|_| {},
|_| {}
);
}
#[test]
fn test_modify_purge_eckey() {
let private_key = EcKey::generate(&DEFAULT_KEY_GROUP).unwrap();
let private_key_value = Value::EcKeyPrivate(private_key.clone());
let uuid = Uuid::new_v4();
let ea = entry_init!(
("class", Value::new_class("account")),
("class", Value::new_class("person")),
("class", Value::new_class("object")),
("name", Value::new_iname("test_name")),
("uuid", Value::Uuid(uuid)),
("id_verification_eckey", private_key_value.clone()),
("description", Value::new_utf8s("testperson")),
("displayname", Value::new_utf8s("Test person!"))
);
let key_partialvalue = valueset::from_value_iter(std::iter::once(private_key_value))
.unwrap()
.to_partialvalue_iter()
.next()
.unwrap();
let preload = vec![ea];
run_modify_test!(
Ok(()),
preload,
filter!(f_eq("name", PartialValue::new_iname("test_name"))),
modlist!([m_purge("id_verification_eckey")]),
None,
|_| {},
|qs: &mut QueryServerWriteTransaction| {
let e = qs.internal_search_uuid(uuid).expect("failed to get entry");
assert!(
!e.attribute_equality("id_verification_eckey", &key_partialvalue)
&& e.attribute_pres("id_verification_eckey")
)
}
);
}
#[test]
fn test_modify_remove_eckey() {
let private_key = EcKey::generate(&DEFAULT_KEY_GROUP).unwrap();
let private_key_value = Value::EcKeyPrivate(private_key.clone());
let uuid = Uuid::new_v4();
let ea = entry_init!(
("class", Value::new_class("account")),
("class", Value::new_class("person")),
("class", Value::new_class("object")),
("name", Value::new_iname("test_name")),
("uuid", Value::Uuid(uuid)),
("id_verification_eckey", private_key_value.clone()),
("description", Value::new_utf8s("testperson")),
("displayname", Value::new_utf8s("Test person!"))
);
let key_partialvalue = valueset::from_value_iter(std::iter::once(private_key_value))
.unwrap()
.to_partialvalue_iter()
.next()
.unwrap();
let preload = vec![ea];
run_modify_test!(
Ok(()),
preload,
filter!(f_eq("name", PartialValue::new_iname("test_name"))),
modlist!([m_remove("id_verification_eckey", &key_partialvalue)]),
None,
|_| {},
|qs: &mut QueryServerWriteTransaction| {
let e = qs.internal_search_uuid(uuid).expect("failed to get entry");
assert!(
!e.attribute_equality("id_verification_eckey", &key_partialvalue)
&& e.attribute_pres("id_verification_eckey")
)
}
);
}
}

View file

@ -16,6 +16,7 @@ mod base;
mod cred_import;
mod domain;
pub(crate) mod dyngroup;
mod eckeygen;
mod gidnumber;
mod jwskeygen;
mod memberof;
@ -24,7 +25,6 @@ mod protected;
mod refint;
mod session;
mod spn;
trait Plugin {
fn id() -> &'static str;
@ -209,6 +209,7 @@ impl Plugins {
.and_then(|_| domain::Domain::pre_create_transform(qs, cand, ce))
.and_then(|_| spn::Spn::pre_create_transform(qs, cand, ce))
.and_then(|_| namehistory::NameHistory::pre_create_transform(qs, cand, ce))
.and_then(|_| eckeygen::EcdhKeyGen::pre_create_transform(qs, cand, ce))
// Should always be last
.and_then(|_| attrunique::AttrUnique::pre_create_transform(qs, cand, ce))
}
@ -248,6 +249,7 @@ impl Plugins {
.and_then(|_| spn::Spn::pre_modify(qs, pre_cand, cand, me))
.and_then(|_| session::SessionConsistency::pre_modify(qs, pre_cand, cand, me))
.and_then(|_| namehistory::NameHistory::pre_modify(qs, pre_cand, cand, me))
.and_then(|_| eckeygen::EcdhKeyGen::pre_modify(qs, pre_cand, cand, me))
// attr unique should always be last
.and_then(|_| attrunique::AttrUnique::pre_modify(qs, pre_cand, cand, me))
}
@ -280,6 +282,7 @@ impl Plugins {
.and_then(|_| spn::Spn::pre_batch_modify(qs, pre_cand, cand, me))
.and_then(|_| session::SessionConsistency::pre_batch_modify(qs, pre_cand, cand, me))
.and_then(|_| namehistory::NameHistory::pre_batch_modify(qs, pre_cand, cand, me))
.and_then(|_| eckeygen::EcdhKeyGen::pre_batch_modify(qs, pre_cand, cand, me))
// attr unique should always be last
.and_then(|_| attrunique::AttrUnique::pre_batch_modify(qs, pre_cand, cand, me))
}

View file

@ -390,6 +390,9 @@ pub enum ReplAttrV1 {
AuditLogString {
set: Vec<(Cid, String)>,
},
EcKeyPrivate {
key: Vec<u8>,
},
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]

View file

@ -210,6 +210,7 @@ impl SchemaAttribute {
SyntaxType::JwsKeyEs256 => matches!(v, PartialValue::Iutf8(_)),
SyntaxType::JwsKeyRs256 => matches!(v, PartialValue::Iutf8(_)),
SyntaxType::UiHint => matches!(v, PartialValue::UiHint(_)),
SyntaxType::EcKeyPrivate => matches!(v, PartialValue::SecretValue),
// Comparing on the label.
SyntaxType::TotpSecret => matches!(v, PartialValue::Utf8(_)),
SyntaxType::AuditLogString => matches!(v, PartialValue::Utf8(_)),
@ -263,6 +264,7 @@ impl SchemaAttribute {
SyntaxType::UiHint => matches!(v, Value::UiHint(_)),
SyntaxType::TotpSecret => matches!(v, Value::TotpSecret(_, _)),
SyntaxType::AuditLogString => matches!(v, Value::Utf8(_)),
SyntaxType::EcKeyPrivate => matches!(v, Value::EcKeyPrivate(_)),
};
if r {
Ok(())

View file

@ -211,6 +211,14 @@ mod tests {
"name_history",
Value::AuditLogString(server_txn.get_txn_cid().clone(), "testperson".to_string()),
);
// this is kinda ugly but since ecdh keys are generated we don't have any other way
let key = r2
.first()
.unwrap()
.get_ava_single_eckey_private("id_verification_eckey")
.unwrap();
e.add_ava("id_verification_eckey", Value::EcKeyPrivate(key.clone()));
let expected = vec![Arc::new(e.into_sealed_committed())];

View file

@ -465,6 +465,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
SCHEMA_ATTR_DOMAIN_TOKEN_KEY.clone().into(),
SCHEMA_ATTR_DOMAIN_UUID.clone().into(),
SCHEMA_ATTR_DYNGROUP_FILTER.clone().into(),
SCHEMA_ATTR_EC_KEY_PRIVATE.clone().into(),
SCHEMA_ATTR_ES256_PRIVATE_KEY_DER.clone().into(),
SCHEMA_ATTR_FERNET_PRIVATE_KEY_STR.clone().into(),
SCHEMA_ATTR_GIDNUMBER.clone().into(),

View file

@ -565,6 +565,7 @@ pub trait QueryServerTransaction<'a> {
.map_err(|()| OperationError::InvalidAttribute("Invalid uihint syntax".to_string())),
SyntaxType::TotpSecret => Err(OperationError::InvalidAttribute("TotpSecret Values can not be supplied through modification".to_string())),
SyntaxType::AuditLogString => Err(OperationError::InvalidAttribute("Audit logs are generated and not able to be set.".to_string())),
SyntaxType::EcKeyPrivate => Err(OperationError::InvalidAttribute("Ec keys are generated and not able to be set.".to_string())),
}
}
None => {
@ -672,6 +673,7 @@ pub trait QueryServerTransaction<'a> {
OperationError::InvalidAttribute("Invalid uihint syntax".to_string())
}),
SyntaxType::AuditLogString => Ok(PartialValue::new_utf8s(value)),
SyntaxType::EcKeyPrivate => Ok(PartialValue::SecretValue),
}
}
None => {

View file

@ -15,6 +15,8 @@ use base64::{engine::general_purpose, Engine as _};
use compact_jwt::JwsSigner;
use hashbrown::HashSet;
use num_enum::TryFromPrimitive;
use openssl::ec::EcKey;
use openssl::pkey::Private;
use regex::Regex;
use serde::{Deserialize, Serialize};
use sshkeys::PublicKey as SshPublicKey;
@ -23,17 +25,17 @@ use url::Url;
use uuid::Uuid;
use webauthn_rs::prelude::{DeviceKey as DeviceKeyV4, Passkey as PasskeyV4};
use kanidm_proto::v1::ApiTokenPurpose;
use kanidm_proto::v1::Filter as ProtoFilter;
use kanidm_proto::v1::UatPurposeStatus;
use kanidm_proto::v1::UiHint;
use crate::be::dbentry::DbIdentSpn;
use crate::credential::{totp::Totp, Credential};
use crate::prelude::*;
use crate::repl::cid::Cid;
use crate::server::identity::IdentityId;
use crate::valueset::uuid_to_proto_string;
use kanidm_proto::v1::ApiTokenPurpose;
use kanidm_proto::v1::Filter as ProtoFilter;
use kanidm_proto::v1::UatPurposeStatus;
use kanidm_proto::v1::UiHint;
use std::hash::Hash;
lazy_static! {
pub static ref SPN_RE: Regex = {
@ -112,7 +114,6 @@ pub struct Address {
// Must be validated.
pub country: String,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct CredUpdateSessionPerms {
pub ext_cred_portal_can_view: bool,
@ -249,6 +250,7 @@ pub enum SyntaxType {
TotpSecret = 30,
ApiToken = 31,
AuditLogString = 32,
EcKeyPrivate = 33,
}
impl TryFrom<&str> for SyntaxType {
@ -291,6 +293,7 @@ impl TryFrom<&str> for SyntaxType {
"TOTPSECRET" => Ok(SyntaxType::TotpSecret),
"APITOKEN" => Ok(SyntaxType::ApiToken),
"AUDIT_LOG_STRING" => Ok(SyntaxType::AuditLogString),
"EC_KEY_PRIVATE" => Ok(SyntaxType::EcKeyPrivate),
_ => Err(()),
}
}
@ -332,6 +335,7 @@ impl fmt::Display for SyntaxType {
SyntaxType::TotpSecret => "TOTPSECRET",
SyntaxType::ApiToken => "APITOKEN",
SyntaxType::AuditLogString => "AUDIT_LOG_STRING",
SyntaxType::EcKeyPrivate => "EC_KEY_PRIVATE",
})
}
}
@ -380,7 +384,6 @@ pub enum PartialValue {
UiHint(UiHint),
Passkey(Uuid),
DeviceKey(Uuid),
// The label, if any.
}
impl From<SyntaxType> for PartialValue {
@ -900,6 +903,7 @@ pub enum Value {
TotpSecret(String, Totp),
AuditLogString(Cid, String),
EcKeyPrivate(EcKey<Private>),
}
impl PartialEq for Value {
@ -1699,6 +1703,7 @@ impl Value {
| Value::Session(_, _)
| Value::Oauth2Session(_, _)
| Value::JwsKeyRs256(_)
| Value::EcKeyPrivate(_)
| Value::UiHint(_) => true,
}
}

View file

@ -0,0 +1,218 @@
use std::iter::{self};
use crate::be::dbvalue::DbValueSetV2;
use crate::prelude::ValueSetT;
use crate::repl::proto::ReplAttrV1;
use crate::value::{PartialValue, SyntaxType, Value};
use kanidm_proto::v1::OperationError;
use openssl::ec::EcKey;
use openssl::pkey::{Private, Public};
use super::ValueSet;
#[derive(Debug, Clone)]
struct EcKeyPrivate {
priv_key: EcKey<Private>,
pub_key: EcKey<Public>,
}
#[derive(Debug, Clone)]
pub struct ValueSetEcKeyPrivate {
set: Option<EcKeyPrivate>,
}
impl ValueSetEcKeyPrivate {
pub fn new(key: &EcKey<Private>) -> Box<Self> {
#[allow(clippy::expect_used)]
let pub_key = Self::private_key_to_public_key(key).expect(
"Unable to retrieve public key from private key, likely corrupted. You must restore from backup.",
);
Box::new(ValueSetEcKeyPrivate {
set: Some(EcKeyPrivate {
priv_key: key.clone(),
pub_key,
}),
})
}
fn push(&mut self, key: &EcKey<Private>) -> bool {
#[allow(clippy::expect_used)]
let pub_key = Self::private_key_to_public_key(key).expect(
"Unable to retrieve public key from private key, likely corrupted. You must restore from backup.",
);
self.set = Some(EcKeyPrivate {
priv_key: key.clone(),
pub_key,
});
true
}
fn valueset_from_key_der(key_der: &[u8]) -> Result<ValueSet, OperationError> {
let option_key = EcKey::private_key_from_der(key_der);
if let Ok(key) = option_key {
Ok(Self::new(&key))
} else {
Err(OperationError::InvalidDbState)
}
}
fn private_key_to_public_key(private_key: &EcKey<Private>) -> Option<EcKey<Public>> {
let public_key = private_key.public_key();
let group = private_key.group();
EcKey::from_public_key(group, public_key).ok()
}
pub fn from_dbvs2(key_der: &[u8]) -> Result<ValueSet, OperationError> {
Self::valueset_from_key_der(key_der)
}
pub fn from_repl_v1(key_der: &[u8]) -> Result<ValueSet, OperationError> {
Self::valueset_from_key_der(key_der)
}
}
impl ValueSetT for ValueSetEcKeyPrivate {
fn insert_checked(
&mut self,
value: crate::value::Value,
) -> Result<bool, kanidm_proto::v1::OperationError> {
match value {
Value::EcKeyPrivate(k) => Ok(self.push(&k)),
_ => {
debug_assert!(false);
Err(OperationError::InvalidValueState)
}
}
}
fn clear(&mut self) {
self.set = None;
}
fn remove(&mut self, _pv: &crate::value::PartialValue) -> bool {
false
}
fn contains(&self, _pv: &crate::value::PartialValue) -> bool {
false
}
fn substring(&self, _pv: &crate::value::PartialValue) -> bool {
false
}
fn lessthan(&self, _pv: &crate::value::PartialValue) -> bool {
false
}
fn len(&self) -> usize {
1
}
fn generate_idx_eq_keys(&self) -> Vec<String> {
Vec::with_capacity(0)
}
fn syntax(&self) -> SyntaxType {
SyntaxType::EcKeyPrivate
}
fn validate(&self, _schema_attr: &crate::schema::SchemaAttribute) -> bool {
match self.set.as_ref() {
Some(key) => key.priv_key.check_key().is_ok() && key.pub_key.check_key().is_ok(),
None => false,
}
}
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
Box::new(iter::once(String::from("hidden")))
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
#[allow(clippy::expect_used)]
let key_der = self
.set
.as_ref()
.map(|key| {
key.priv_key.private_key_to_der().expect(
"Unable to process eckey to der, likely corrupted. You must restore from backup.",
)
})
.unwrap_or_default();
DbValueSetV2::EcKeyPrivate(key_der)
}
fn to_repl_v1(&self) -> ReplAttrV1 {
#[allow(clippy::expect_used)]
let key_der = self
.set
.as_ref()
.map(|key| {
key.priv_key.private_key_to_der().expect(
"Unable to process eckey to der, likely corrupted. You must restore from backup.",
)
})
.unwrap_or_default();
ReplAttrV1::EcKeyPrivate { key: key_der }
}
fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = crate::value::PartialValue> + '_> {
Box::new(iter::once(PartialValue::SecretValue))
}
fn to_value_iter(&self) -> Box<dyn Iterator<Item = crate::value::Value> + '_> {
match &self.set {
Some(key) => Box::new(iter::once(Value::EcKeyPrivate(key.priv_key.clone()))),
None => Box::new(iter::empty::<Value>()),
}
}
fn equal(&self, other: &super::ValueSet) -> bool {
#[allow(clippy::expect_used)]
other.as_ec_key_private().map_or(false, |other_key| {
self.set.as_ref().map_or(false, |key| {
key.priv_key
.private_key_to_der()
.expect("Failed to retrieve key der")
== other_key
.private_key_to_der()
.expect("Failed to retrieve key der")
})
})
}
fn merge(&mut self, other: &super::ValueSet) -> Result<(), kanidm_proto::v1::OperationError> {
if let Some(other_key) = other.as_ec_key_private() {
let priv_key = other_key.clone();
let pub_key = Self::private_key_to_public_key(&priv_key)
.ok_or(OperationError::CryptographyError)?;
self.set = Some(EcKeyPrivate { pub_key, priv_key });
Ok(())
} else {
debug_assert!(false);
Err(OperationError::InvalidValueState)
}
}
fn as_ec_key_private(&self) -> Option<&EcKey<Private>> {
match self.set.as_ref() {
Some(key) => Some(&key.priv_key),
None => None,
}
}
fn to_eckey_private_single(&self) -> Option<&EcKey<Private>> {
match self.set.as_ref() {
Some(key) => Some(&key.priv_key),
None => None,
}
}
fn to_eckey_public_single(&self) -> Option<&EcKey<Public>> {
match self.set.as_ref() {
Some(key) => Some(&key.pub_key),
None => None,
}
}
}

View file

@ -3,6 +3,9 @@ use std::collections::{BTreeMap, BTreeSet};
use compact_jwt::JwsSigner;
use dyn_clone::DynClone;
use hashbrown::HashSet;
use openssl::ec::EcKey;
use openssl::pkey::Private;
use openssl::pkey::Public;
use smolset::SmolSet;
use time::OffsetDateTime;
// use std::fmt::Debug;
@ -26,6 +29,7 @@ pub use self::bool::ValueSetBool;
pub use self::cid::ValueSetCid;
pub use self::cred::{ValueSetCredential, ValueSetDeviceKey, ValueSetIntentToken, ValueSetPasskey};
pub use self::datetime::ValueSetDateTime;
pub use self::eckey::ValueSetEcKeyPrivate;
pub use self::iname::ValueSetIname;
pub use self::index::ValueSetIndex;
pub use self::iutf8::ValueSetIutf8;
@ -53,6 +57,7 @@ mod bool;
mod cid;
mod cred;
mod datetime;
pub mod eckey;
mod iname;
mod index;
mod iutf8;
@ -503,6 +508,16 @@ pub trait ValueSetT: std::fmt::Debug + DynClone {
None
}
fn to_eckey_private_single(&self) -> Option<&EcKey<Private>> {
debug_assert!(false);
None
}
fn to_eckey_public_single(&self) -> Option<&EcKey<Public>> {
debug_assert!(false);
None
}
fn as_jws_key_es256_set(&self) -> Option<&HashSet<JwsSigner>> {
debug_assert!(false);
None
@ -527,11 +542,17 @@ pub trait ValueSetT: std::fmt::Debug + DynClone {
debug_assert!(false);
None
}
fn as_audit_log_string(&self) -> Option<&SmolSet<[(Cid, String); 8]>> {
debug_assert!(false);
None
}
fn as_ec_key_private(&self) -> Option<&EcKey<Private>> {
debug_assert!(false);
None
}
fn repl_merge_valueset(
&self,
_older: &ValueSet,
@ -605,6 +626,7 @@ pub fn from_result_value_iter(
Value::EmailAddress(a, _) => ValueSetEmailAddress::new(a),
Value::UiHint(u) => ValueSetUiHint::new(u),
Value::AuditLogString(c, s) => ValueSetAuditLogString::new((c, s)),
Value::EcKeyPrivate(k) => ValueSetEcKeyPrivate::new(&k),
Value::PhoneNumber(_, _)
| Value::Passkey(_, _, _)
| Value::DeviceKey(_, _, _)
@ -670,6 +692,7 @@ pub fn from_value_iter(mut iter: impl Iterator<Item = Value>) -> Result<ValueSet
Value::UiHint(u) => ValueSetUiHint::new(u),
Value::TotpSecret(l, t) => ValueSetTotpSecret::new(l, t),
Value::AuditLogString(c, s) => ValueSetAuditLogString::new((c, s)),
Value::EcKeyPrivate(k) => ValueSetEcKeyPrivate::new(&k),
Value::PhoneNumber(_, _) => {
debug_assert!(false);
return Err(OperationError::InvalidValueState);
@ -720,6 +743,7 @@ pub fn from_db_valueset_v2(dbvs: DbValueSetV2) -> Result<ValueSet, OperationErro
DbValueSetV2::UiHint(set) => ValueSetUiHint::from_dbvs2(set),
DbValueSetV2::TotpSecret(set) => ValueSetTotpSecret::from_dbvs2(set),
DbValueSetV2::AuditLogString(set) => ValueSetAuditLogString::from_dbvs2(set),
DbValueSetV2::EcKeyPrivate(key) => ValueSetEcKeyPrivate::from_dbvs2(&key),
DbValueSetV2::PhoneNumber(_, _) | DbValueSetV2::TrustedDeviceEnrollment(_) => {
debug_assert!(false);
Err(OperationError::InvalidValueState)
@ -767,5 +791,6 @@ pub fn from_repl_v1(rv1: &ReplAttrV1) -> Result<ValueSet, OperationError> {
ReplAttrV1::ApiToken { set } => ValueSetApiToken::from_repl_v1(set),
ReplAttrV1::TotpSecret { set } => ValueSetTotpSecret::from_repl_v1(set),
ReplAttrV1::AuditLogString { set } => ValueSetAuditLogString::from_repl_v1(set),
ReplAttrV1::EcKeyPrivate { key } => ValueSetEcKeyPrivate::from_repl_v1(key),
}
}

View file

@ -38,6 +38,7 @@ sketching = { workspace = true }
testkit-macros = { workspace = true }
tracing = { workspace = true, features = ["attributes"] }
tokio = { workspace = true, features = ["net", "sync", "io-util", "macros"] }
openssl.workspace = true
[build-dependencies]

View file

@ -0,0 +1,321 @@
use core::result::Result::Err;
use kanidm_client::KanidmClient;
use kanidm_proto::{
internal::{IdentifyUserRequest, IdentifyUserResponse},
v1::Entry,
};
use kanidmd_testkit::ADMIN_TEST_PASSWORD;
use reqwest::StatusCode;
static UNIVERSAL_PW: &'static str = "eicieY7ahchaoCh0eeTa";
static UNIVERSAL_PW_HASH: &'static str =
"pbkdf2_sha256$36000$xIEozuZVAoYm$uW1b35DUKyhvQAf1mBqMvoBDcqSD06juzyO/nmyV0+w=";
static USER_A_NAME: &'static str = "valid_user_a";
static USER_B_NAME: &'static str = "valid_user_b";
// TEST ON ERROR OUTCOMES
// These tests check that invalid requests return the expected error
#[kanidmd_testkit::test]
async fn test_not_authenticated(rsclient: KanidmClient) {
// basically here we try a bit of all the possible combinations while unauthenticated to check it's not working
setup_server(&rsclient).await;
create_user(&rsclient, USER_A_NAME).await;
let _ = rsclient.logout().await;
let res = rsclient
.idm_person_identify_user(USER_A_NAME, IdentifyUserRequest::Start)
.await;
assert!(
matches!(res, Err(err) if matches!(err, kanidm_client::ClientError::Http(reqwest::StatusCode::UNAUTHORIZED, ..)))
);
let res = rsclient
.idm_person_identify_user(USER_A_NAME, IdentifyUserRequest::DisplayCode)
.await;
assert!(
matches!(res, Err(err) if matches!(err, kanidm_client::ClientError::Http(reqwest::StatusCode::UNAUTHORIZED, ..)))
);
let res = rsclient
.idm_person_identify_user(
USER_A_NAME,
IdentifyUserRequest::SubmitCode { other_totp: 123456 },
)
.await;
assert!(
matches!(res, Err(err) if matches!(err, kanidm_client::ClientError::Http(reqwest::StatusCode::UNAUTHORIZED, ..)))
);
}
#[kanidmd_testkit::test]
async fn test_non_existing_user_id(rsclient: KanidmClient) {
setup_server(&rsclient).await;
create_user(&rsclient, USER_A_NAME).await;
create_user(&rsclient, USER_B_NAME).await;
let non_existing_user = "non_existing_user";
login_with_user(&rsclient, USER_A_NAME).await;
let res: Result<IdentifyUserResponse, kanidm_client::ClientError> = rsclient
.idm_person_identify_user(non_existing_user, IdentifyUserRequest::Start)
.await;
assert!(
matches!(dbg!(res), Err(err) if matches!(err, kanidm_client::ClientError::Http(StatusCode::NOT_FOUND, Some(kanidm_proto::v1::OperationError::NoMatchingEntries), .. )))
);
let res = rsclient
.idm_person_identify_user(non_existing_user, IdentifyUserRequest::DisplayCode)
.await;
assert!(
matches!(dbg!(res), Err(err) if matches!(err, kanidm_client::ClientError::Http(StatusCode::NOT_FOUND, Some(kanidm_proto::v1::OperationError::NoMatchingEntries), .. )))
);
let res = rsclient
.idm_person_identify_user(
non_existing_user,
IdentifyUserRequest::SubmitCode { other_totp: 123456 },
)
.await;
assert!(
matches!(dbg!(res), Err(err) if matches!(err, kanidm_client::ClientError::Http(StatusCode::NOT_FOUND, Some(kanidm_proto::v1::OperationError::NoMatchingEntries), .. )))
);
}
// TEST ON SPECIFIC API INPUT
// These tests check that given a specific input we get the expected response.
// WE DON'T CHECK THE CONTENT OF THE RESPONSE, just that it's the expected one.
// The api tests from here on should never return any error, as all the
// error cases have already been tested in the previous section!
// Each tests is named like `test_{api input}_response_{expected api output}_or_{expected api output}`
#[kanidmd_testkit::test]
async fn test_start_response_identity_verification_available(rsclient: KanidmClient) {
setup_server(&rsclient).await;
create_user(&rsclient, USER_A_NAME).await;
login_with_user(&rsclient, USER_A_NAME).await;
let response = rsclient
.idm_person_identify_user(USER_A_NAME, IdentifyUserRequest::Start)
.await;
assert!(response.is_ok());
// since we sent our own identifier here it should just tell us that we that we can use the feature
assert_eq!(
response.unwrap(),
IdentifyUserResponse::IdentityVerificationAvailable
)
}
// this function tests both possible POSITIVE outcomes if we start from
// `Start`, that is WaitForCode or ProvideCode
#[kanidmd_testkit::test]
async fn test_start_response_wait_for_code_or_provide_code(rsclient: KanidmClient) {
setup_server(&rsclient).await;
let user_a_uuid = create_user(&rsclient, USER_A_NAME).await;
let user_b_uuid = create_user(&rsclient, USER_B_NAME).await;
login_with_user(&rsclient, USER_A_NAME).await;
let response = rsclient
.idm_person_identify_user(USER_B_NAME, IdentifyUserRequest::Start)
.await;
assert!(response.is_ok());
// the person with the lowest uuid should get to input the other person's code first;
dbg!(user_a_uuid.clone(), user_b_uuid.clone());
if user_a_uuid < user_b_uuid {
assert_eq!(response.unwrap(), IdentifyUserResponse::WaitForCode);
} else {
assert!(matches!(
response.unwrap(),
IdentifyUserResponse::ProvideCode { .. }
))
}
}
#[kanidmd_testkit::test]
async fn test_provide_code_response_code_failure_or_provide_code(rsclient: KanidmClient) {
setup_server(&rsclient).await;
let user_a_uuid = create_user(&rsclient, USER_A_NAME).await;
let user_b_uuid = create_user(&rsclient, USER_B_NAME).await;
login_with_user(&rsclient, USER_A_NAME).await;
let response = rsclient
.idm_person_identify_user(
USER_B_NAME,
IdentifyUserRequest::SubmitCode { other_totp: 123456 },
)
.await;
//if A is the first then either the code is correct and therefore we get a ProvideCode or it's wrong
// and we get a CodeFailure
if user_a_uuid < user_b_uuid {
assert!(matches!(
response.unwrap(),
IdentifyUserResponse::ProvideCode { .. } | IdentifyUserResponse::CodeFailure
));
} else {
assert!(matches!(
response.unwrap(),
IdentifyUserResponse::Success | IdentifyUserResponse::CodeFailure
));
}
}
// here we actually test the full idm flow by duplicating the server
#[kanidmd_testkit::test]
async fn test_full_identification_flow(rsclient: KanidmClient) {
setup_server(&rsclient).await;
let user_a_uuid = create_user(&rsclient, USER_A_NAME).await;
let user_b_uuid = create_user(&rsclient, USER_B_NAME).await;
//user A session
let valid_user_a_client = rsclient;
login_with_user(&valid_user_a_client, USER_A_NAME).await;
//user B session
let valid_user_b_client = valid_user_a_client.new_session().unwrap();
login_with_user(&valid_user_b_client, USER_B_NAME).await;
// now we have to consider the two separate cases: first we address the case a has the lowest uuid
let (lower_user_client, lower_user_name, higher_user_client, higher_user_name) =
if user_a_uuid < user_b_uuid {
(
valid_user_a_client,
USER_A_NAME,
valid_user_b_client,
USER_B_NAME,
)
} else {
(
valid_user_b_client,
USER_B_NAME,
valid_user_a_client,
USER_A_NAME,
)
};
let lower_user_req_1 = lower_user_client
.idm_person_identify_user(higher_user_name, IdentifyUserRequest::Start)
.await
.unwrap();
let higher_user_req_1 = higher_user_client
.idm_person_identify_user(lower_user_name, IdentifyUserRequest::Start)
.await
.unwrap();
assert_eq!(lower_user_req_1, IdentifyUserResponse::WaitForCode);
// we check that the user A got a WaitForCode
let IdentifyUserResponse::ProvideCode { step: _, totp } = higher_user_req_1 else {
return assert!(false);
// we check that the user B got the code
};
// we now try to submit the wrong code and we check that we get CodeFailure
// we now submit the received totp as the user A
let lower_user_req_2_wrong = lower_user_client
.idm_person_identify_user(
higher_user_name,
IdentifyUserRequest::SubmitCode {
other_totp: totp + 1,
},
)
.await
.unwrap();
assert_eq!(lower_user_req_2_wrong, IdentifyUserResponse::CodeFailure);
// now we do it using the right totp
let lower_user_req_2_right = lower_user_client
.idm_person_identify_user(
higher_user_name,
IdentifyUserRequest::SubmitCode { other_totp: totp },
)
.await
.unwrap();
// if the totp was correct we must get a ProvideCode
let IdentifyUserResponse::ProvideCode { step: _, totp } = lower_user_req_2_right else {
return assert!(false)
};
// we now try to do the same thing with user B: we first submit the wrong code expecting CodeFailure,
// and then we submit the right one expecting Success
let higher_user_req_2_wrong = higher_user_client
.idm_person_identify_user(
lower_user_name,
IdentifyUserRequest::SubmitCode {
other_totp: totp + 1,
},
)
.await
.unwrap();
assert_eq!(higher_user_req_2_wrong, IdentifyUserResponse::CodeFailure);
// now we do it using the right totp
let higher_user_req_2_right = higher_user_client
.idm_person_identify_user(
lower_user_name,
IdentifyUserRequest::SubmitCode { other_totp: totp },
)
.await
.unwrap();
// since user B has already provided their code this is their last action and they must get a Success if
// the provided code is correct
assert_eq!(higher_user_req_2_right, IdentifyUserResponse::Success);
}
async fn setup_server(rsclient: &KanidmClient) {
// basically this function logs in
let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await;
assert!(res.is_ok());
// To enable the admin to actually make some of these changes, we have
// to make them a people admin. NOT recommended in production!
rsclient
.idm_group_add_members("idm_people_account_password_import_priv", &["admin"])
.await
.unwrap();
rsclient
.idm_group_add_members("idm_people_manage_priv", &["admin"])
.await
.unwrap();
rsclient
.idm_group_add_members("idm_admins", &["admin"])
.await
.unwrap();
}
async fn create_user(rsclient: &KanidmClient, user: &str) -> String {
let e: Entry = serde_json::from_str(&format!(
r#"{{
"attrs": {{
"class": ["account", "person", "object"],
"name": ["{}"],
"displayname": ["dx{}"]
}}
}}"#,
user, user
))
.unwrap();
let res = rsclient.create(vec![e.clone()]).await;
assert!(res.is_ok());
rsclient
.idm_person_account_primary_credential_import_password(user, UNIVERSAL_PW_HASH)
.await
.unwrap();
let r = rsclient
.idm_person_account_get_attr(user, "uuid")
.await
.unwrap();
r.unwrap().first().unwrap().to_owned()
}
async fn login_with_user(rsclient: &KanidmClient, id: &str) {
let _ = rsclient.logout().await;
let res = rsclient.auth_simple_password(id, UNIVERSAL_PW).await;
assert!(res.is_ok());
}

View file

@ -7,7 +7,7 @@ version = "1.1.0-rc.14-dev"
authors = [
"William Brown <william@blackhats.net.au>",
"James Hodgkinson <james@terminaloutcomes.com>",
]
]
rust-version = "1.66"
edition = "2021"
license = "MPL-2.0"
@ -32,6 +32,10 @@ uuid = { workspace = true }
yew = { workspace = true, features = ["csr"] }
yew-router = { workspace = true }
time = { workspace = true }
gloo-timers = "0.2.6"
wasm-timer = "0.2.5"
regex.workspace = true
lazy_static.workspace = true
[dependencies.web-sys]
workspace = true

View file

@ -22,9 +22,7 @@ function takeObject(idx) {
return ret;
}
const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
let WASM_VECTOR_LEN = 0;
let cachedUint8Memory0 = null;
@ -35,22 +33,6 @@ function getUint8Memory0() {
return cachedUint8Memory0;
}
function getStringFromWasm0(ptr, len) {
ptr = ptr >>> 0;
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
}
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 WASM_VECTOR_LEN = 0;
const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } );
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
@ -117,6 +99,24 @@ function getInt32Memory0() {
return cachedInt32Memory0;
}
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;
}
const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
function getStringFromWasm0(ptr, len) {
ptr = ptr >>> 0;
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
}
let cachedFloat64Memory0 = null;
function getFloat64Memory0() {
@ -224,6 +224,9 @@ function makeMutClosure(arg0, arg1, dtor, f) {
return real;
}
function __wbg_adapter_48(arg0, arg1) {
wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h0b7f7280fe11554e(arg0, arg1);
}
let stack_pointer = 128;
@ -232,24 +235,24 @@ function addBorrowedObject(obj) {
heap[--stack_pointer] = obj;
return stack_pointer;
}
function __wbg_adapter_48(arg0, arg1, arg2) {
try {
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hc988533207355089(arg0, arg1, addBorrowedObject(arg2));
} finally {
heap[stack_pointer++] = undefined;
}
}
function __wbg_adapter_51(arg0, arg1, arg2) {
try {
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__ha34c6831d0d6d86f(arg0, arg1, addBorrowedObject(arg2));
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h575e9f970b01eb58(arg0, arg1, addBorrowedObject(arg2));
} finally {
heap[stack_pointer++] = undefined;
}
}
function __wbg_adapter_54(arg0, arg1, arg2) {
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h52363d7e83a077de(arg0, arg1, addHeapObject(arg2));
try {
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hb3e22b40c036b75b(arg0, arg1, addBorrowedObject(arg2));
} finally {
heap[stack_pointer++] = undefined;
}
}
function __wbg_adapter_57(arg0, arg1, arg2) {
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hc278baeffa5598ad(arg0, arg1, addHeapObject(arg2));
}
/**
@ -270,6 +273,14 @@ export function run_app() {
}
}
function handleError(f, args) {
try {
return f.apply(this, args);
} catch (e) {
wasm.__wbindgen_exn_store(addHeapObject(e));
}
}
let cachedUint32Memory0 = null;
function getUint32Memory0() {
@ -290,14 +301,6 @@ function getArrayJsValueFromWasm0(ptr, len) {
return result;
}
function handleError(f, args) {
try {
return f.apply(this, args);
} catch (e) {
wasm.__wbindgen_exn_store(addHeapObject(e));
}
}
async function __wbg_load(module, imports) {
if (typeof Response === 'function' && module instanceof Response) {
if (typeof WebAssembly.instantiateStreaming === 'function') {
@ -344,10 +347,6 @@ function __wbg_get_imports() {
const ret = false;
return ret;
};
imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
const ret = getStringFromWasm0(arg0, arg1);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_string_get = function(arg0, arg1) {
const obj = getObject(arg1);
const ret = typeof(obj) === 'string' ? obj : undefined;
@ -356,12 +355,16 @@ function __wbg_get_imports() {
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
};
imports.wbg.__wbg_modalhidebyid_a36f33eb8222a059 = function(arg0, arg1) {
modal_hide_by_id(getStringFromWasm0(arg0, arg1));
};
imports.wbg.__wbindgen_object_clone_ref = function(arg0) {
const ret = getObject(arg0);
return addHeapObject(ret);
};
imports.wbg.__wbg_modalhidebyid_a36f33eb8222a059 = function(arg0, arg1) {
modal_hide_by_id(getStringFromWasm0(arg0, arg1));
imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
const ret = getStringFromWasm0(arg0, arg1);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_error_new = function(arg0, arg1) {
const ret = new Error(getStringFromWasm0(arg0, arg1));
@ -411,14 +414,11 @@ function __wbg_get_imports() {
const ret = getObject(arg0) === undefined;
return ret;
};
imports.wbg.__wbg_listenerid_12315eee21527820 = function(arg0, arg1) {
const ret = getObject(arg1).__yew_listener_id;
imports.wbg.__wbg_cachekey_b61393159c57fd7b = function(arg0, arg1) {
const ret = getObject(arg1).__yew_subtree_cache_key;
getInt32Memory0()[arg0 / 4 + 1] = isLikeNone(ret) ? 0 : ret;
getInt32Memory0()[arg0 / 4 + 0] = !isLikeNone(ret);
};
imports.wbg.__wbg_setlistenerid_3183aae8fa5840fb = function(arg0, arg1) {
getObject(arg0).__yew_listener_id = arg1 >>> 0;
};
imports.wbg.__wbg_subtreeid_e348577f7ef777e3 = function(arg0, arg1) {
const ret = getObject(arg1).__yew_subtree_id;
getInt32Memory0()[arg0 / 4 + 1] = isLikeNone(ret) ? 0 : ret;
@ -427,13 +427,16 @@ function __wbg_get_imports() {
imports.wbg.__wbg_setsubtreeid_d32e6327eef1f7fc = function(arg0, arg1) {
getObject(arg0).__yew_subtree_id = arg1 >>> 0;
};
imports.wbg.__wbg_cachekey_b61393159c57fd7b = function(arg0, arg1) {
const ret = getObject(arg1).__yew_subtree_cache_key;
imports.wbg.__wbg_setcachekey_80183b7cfc421143 = function(arg0, arg1) {
getObject(arg0).__yew_subtree_cache_key = arg1 >>> 0;
};
imports.wbg.__wbg_listenerid_12315eee21527820 = function(arg0, arg1) {
const ret = getObject(arg1).__yew_listener_id;
getInt32Memory0()[arg0 / 4 + 1] = isLikeNone(ret) ? 0 : ret;
getInt32Memory0()[arg0 / 4 + 0] = !isLikeNone(ret);
};
imports.wbg.__wbg_setcachekey_80183b7cfc421143 = function(arg0, arg1) {
getObject(arg0).__yew_subtree_cache_key = arg1 >>> 0;
imports.wbg.__wbg_setlistenerid_3183aae8fa5840fb = function(arg0, arg1) {
getObject(arg0).__yew_listener_id = arg1 >>> 0;
};
imports.wbg.__wbg_new_abda76e883ba8a5f = function() {
const ret = new Error();
@ -457,6 +460,22 @@ function __wbg_get_imports() {
wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
}
};
imports.wbg.__wbg_clearTimeout_76877dbc010e786d = function(arg0) {
const ret = clearTimeout(takeObject(arg0));
return addHeapObject(ret);
};
imports.wbg.__wbg_setTimeout_75cb9b6991a4031d = function() { return handleError(function (arg0, arg1) {
const ret = setTimeout(getObject(arg0), arg1);
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_clearInterval_bd072ecb096d9775 = function(arg0) {
const ret = clearInterval(takeObject(arg0));
return addHeapObject(ret);
};
imports.wbg.__wbg_setInterval_edede8e2124cbb00 = function() { return handleError(function (arg0, arg1) {
const ret = setInterval(getObject(arg0), arg1);
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbindgen_number_new = function(arg0) {
const ret = arg0;
return addHeapObject(ret);
@ -564,16 +583,60 @@ function __wbg_get_imports() {
const ret = getObject(arg0).fetch(getObject(arg1));
return addHeapObject(ret);
};
imports.wbg.__wbg_value_3c5f08ffc2b7d6f9 = function(arg0, arg1) {
const ret = getObject(arg1).value;
imports.wbg.__wbg_addEventListener_a5963e26cd7b176b = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).addEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3), getObject(arg4));
}, arguments) };
imports.wbg.__wbg_removeEventListener_782040b4432709cb = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).removeEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3), arg4 !== 0);
}, arguments) };
imports.wbg.__wbg_instanceof_HtmlFormElement_b57527983c7c1ada = function(arg0) {
let result;
try {
result = getObject(arg0) instanceof HTMLFormElement;
} catch {
result = false;
}
const ret = result;
return ret;
};
imports.wbg.__wbg_href_d62a28e4fc1ab948 = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg1).href;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
};
imports.wbg.__wbg_setvalue_0dc100d4b9908028 = function(arg0, arg1, arg2) {
getObject(arg0).value = getStringFromWasm0(arg1, arg2);
};
}, arguments) };
imports.wbg.__wbg_pathname_c8fd5c498079312d = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg1).pathname;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
}, arguments) };
imports.wbg.__wbg_search_6c3c472e076ee010 = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg1).search;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
}, arguments) };
imports.wbg.__wbg_hash_a1a795b89dda8e3d = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg1).hash;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
}, arguments) };
imports.wbg.__wbg_replace_5d1d2b7956cafd7b = function() { return handleError(function (arg0, arg1, arg2) {
getObject(arg0).replace(getStringFromWasm0(arg1, arg2));
}, arguments) };
imports.wbg.__wbg_state_745dc4814d321eb3 = function() { return handleError(function (arg0) {
const ret = getObject(arg0).state;
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_pushState_1145414a47c0b629 = 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_instanceof_ShadowRoot_b64337370f59fe2d = function(arg0) {
let result;
try {
@ -588,20 +651,6 @@ function __wbg_get_imports() {
const ret = getObject(arg0).host;
return addHeapObject(ret);
};
imports.wbg.__wbg_state_745dc4814d321eb3 = function() { return handleError(function (arg0) {
const ret = getObject(arg0).state;
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_pushState_1145414a47c0b629 = 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_href_47b90f0ddf3ddcd7 = function(arg0, arg1) {
const ret = getObject(arg1).href;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
};
imports.wbg.__wbg_getItem_ed8e218e51f1efeb = function() { return handleError(function (arg0, arg1, arg2, arg3) {
const ret = getObject(arg1).getItem(getStringFromWasm0(arg2, arg3));
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
@ -615,39 +664,33 @@ function __wbg_get_imports() {
imports.wbg.__wbg_setItem_d002ee486462bfff = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).setItem(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
}, arguments) };
imports.wbg.__wbg_instanceof_HtmlInputElement_31b50e0cf542c524 = function(arg0) {
let result;
try {
result = getObject(arg0) instanceof HTMLInputElement;
} catch {
result = false;
}
const ret = result;
return ret;
};
imports.wbg.__wbg_checked_5ccb3a66eb054121 = function(arg0) {
const ret = getObject(arg0).checked;
return ret;
};
imports.wbg.__wbg_setchecked_e5a50baea447b8a8 = function(arg0, arg1) {
getObject(arg0).checked = arg1 !== 0;
};
imports.wbg.__wbg_value_9423da9d988ee8cf = function(arg0, arg1) {
imports.wbg.__wbg_value_3c5f08ffc2b7d6f9 = function(arg0, arg1) {
const ret = getObject(arg1).value;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
};
imports.wbg.__wbg_setvalue_1f95e61cbc382f7f = function(arg0, arg1, arg2) {
imports.wbg.__wbg_setvalue_0dc100d4b9908028 = function(arg0, arg1, arg2) {
getObject(arg0).value = getStringFromWasm0(arg1, arg2);
};
imports.wbg.__wbg_add_3eafedc4b2a28db0 = function() { return handleError(function (arg0, arg1, arg2) {
getObject(arg0).add(getStringFromWasm0(arg1, arg2));
imports.wbg.__wbg_get_2e9aab260014946d = function() { return handleError(function (arg0, arg1, arg2, arg3) {
const ret = getObject(arg1).get(getStringFromWasm0(arg2, arg3));
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
}, arguments) };
imports.wbg.__wbg_remove_8ae45e50cb58bb66 = function() { return handleError(function (arg0, arg1, arg2) {
getObject(arg0).remove(getStringFromWasm0(arg1, arg2));
imports.wbg.__wbg_set_b34caba58723c454 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).set(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
}, arguments) };
imports.wbg.__wbg_href_47b90f0ddf3ddcd7 = function(arg0, arg1) {
const ret = getObject(arg1).href;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
};
imports.wbg.__wbg_instanceof_Element_4622f5da1249a3eb = function(arg0) {
let result;
try {
@ -689,6 +732,36 @@ function __wbg_get_imports() {
imports.wbg.__wbg_setAttribute_e7e80b478b7b8b2f = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).setAttribute(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
}, arguments) };
imports.wbg.__wbg_instanceof_HtmlInputElement_31b50e0cf542c524 = function(arg0) {
let result;
try {
result = getObject(arg0) instanceof HTMLInputElement;
} catch {
result = false;
}
const ret = result;
return ret;
};
imports.wbg.__wbg_checked_5ccb3a66eb054121 = function(arg0) {
const ret = getObject(arg0).checked;
return ret;
};
imports.wbg.__wbg_setchecked_e5a50baea447b8a8 = function(arg0, arg1) {
getObject(arg0).checked = arg1 !== 0;
};
imports.wbg.__wbg_value_9423da9d988ee8cf = function(arg0, arg1) {
const ret = getObject(arg1).value;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
};
imports.wbg.__wbg_setvalue_1f95e61cbc382f7f = function(arg0, arg1, arg2) {
getObject(arg0).value = getStringFromWasm0(arg1, arg2);
};
imports.wbg.__wbg_log_1d3ae0273d8f4f8a = function(arg0) {
console.log(getObject(arg0));
};
imports.wbg.__wbg_instanceof_HtmlElement_6f4725d4677c7968 = function(arg0) {
let result;
try {
@ -702,16 +775,10 @@ function __wbg_get_imports() {
imports.wbg.__wbg_focus_dbcbbbb2a04c0e1f = function() { return handleError(function (arg0) {
getObject(arg0).focus();
}, arguments) };
imports.wbg.__wbg_get_2e9aab260014946d = function() { return handleError(function (arg0, arg1, arg2, arg3) {
const ret = getObject(arg1).get(getStringFromWasm0(arg2, arg3));
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
}, arguments) };
imports.wbg.__wbg_set_b34caba58723c454 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).set(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
}, arguments) };
imports.wbg.__wbg_credentials_66b6baa89eb03c21 = function(arg0) {
const ret = getObject(arg0).credentials;
return addHeapObject(ret);
};
imports.wbg.__wbg_headers_b439dcff02e808e5 = function(arg0) {
const ret = getObject(arg0).headers;
return addHeapObject(ret);
@ -720,16 +787,12 @@ function __wbg_get_imports() {
const ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_create_c7e40b6b88186cbf = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg0).create(getObject(arg1));
imports.wbg.__wbg_newwithform_368648c82279d486 = function() { return handleError(function (arg0) {
const ret = new FormData(getObject(arg0));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_get_e66794f89dcd7828 = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg0).get(getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_credentials_66b6baa89eb03c21 = function(arg0) {
const ret = getObject(arg0).credentials;
imports.wbg.__wbg_get_4c356dcef81d58a5 = function(arg0, arg1, arg2) {
const ret = getObject(arg0).get(getStringFromWasm0(arg1, arg2));
return addHeapObject(ret);
};
imports.wbg.__wbg_instanceof_Response_fc4327dbfcdf5ced = function(arg0) {
@ -754,9 +817,14 @@ function __wbg_get_imports() {
const ret = getObject(arg0).json();
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_log_1d3ae0273d8f4f8a = function(arg0) {
console.log(getObject(arg0));
};
imports.wbg.__wbg_create_c7e40b6b88186cbf = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg0).create(getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_get_e66794f89dcd7828 = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg0).get(getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_target_f171e89c61e2bccf = function(arg0) {
const ret = getObject(arg0).target;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
@ -776,49 +844,6 @@ function __wbg_get_imports() {
imports.wbg.__wbg_preventDefault_24104f3f0a54546a = function(arg0) {
getObject(arg0).preventDefault();
};
imports.wbg.__wbg_newwithform_368648c82279d486 = function() { return handleError(function (arg0) {
const ret = new FormData(getObject(arg0));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_get_4c356dcef81d58a5 = function(arg0, arg1, arg2) {
const ret = getObject(arg0).get(getStringFromWasm0(arg1, arg2));
return addHeapObject(ret);
};
imports.wbg.__wbg_href_d62a28e4fc1ab948 = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg1).href;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
}, arguments) };
imports.wbg.__wbg_pathname_c8fd5c498079312d = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg1).pathname;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
}, arguments) };
imports.wbg.__wbg_search_6c3c472e076ee010 = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg1).search;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
}, arguments) };
imports.wbg.__wbg_hash_a1a795b89dda8e3d = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg1).hash;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
}, arguments) };
imports.wbg.__wbg_replace_5d1d2b7956cafd7b = function() { return handleError(function (arg0, arg1, arg2) {
getObject(arg0).replace(getStringFromWasm0(arg1, arg2));
}, arguments) };
imports.wbg.__wbg_getClientExtensionResults_b9108fbba9f54b38 = function(arg0) {
const ret = getObject(arg0).getClientExtensionResults();
return addHeapObject(ret);
};
imports.wbg.__wbg_parentNode_9e53f8b17eb98c9d = function(arg0) {
const ret = getObject(arg0).parentNode;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
@ -857,6 +882,16 @@ function __wbg_get_imports() {
const ret = getObject(arg0).removeChild(getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_add_3eafedc4b2a28db0 = function() { return handleError(function (arg0, arg1, arg2) {
getObject(arg0).add(getStringFromWasm0(arg1, arg2));
}, arguments) };
imports.wbg.__wbg_remove_8ae45e50cb58bb66 = function() { return handleError(function (arg0, arg1, arg2) {
getObject(arg0).remove(getStringFromWasm0(arg1, arg2));
}, arguments) };
imports.wbg.__wbg_getClientExtensionResults_b9108fbba9f54b38 = function(arg0) {
const ret = getObject(arg0).getClientExtensionResults();
return addHeapObject(ret);
};
imports.wbg.__wbg_href_17ed54b321396524 = function(arg0, arg1) {
const ret = getObject(arg1).href;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
@ -899,22 +934,6 @@ function __wbg_get_imports() {
const ret = new URL(getStringFromWasm0(arg0, arg1), getStringFromWasm0(arg2, arg3));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_addEventListener_a5963e26cd7b176b = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).addEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3), getObject(arg4));
}, arguments) };
imports.wbg.__wbg_removeEventListener_782040b4432709cb = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).removeEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3), arg4 !== 0);
}, arguments) };
imports.wbg.__wbg_instanceof_HtmlFormElement_b57527983c7c1ada = function(arg0) {
let result;
try {
result = getObject(arg0) instanceof HTMLFormElement;
} catch {
result = false;
}
const ret = result;
return ret;
};
imports.wbg.__wbg_get_44be0491f933a435 = function(arg0, arg1) {
const ret = getObject(arg0)[arg1 >>> 0];
return addHeapObject(ret);
@ -1046,6 +1065,10 @@ function __wbg_get_imports() {
const ret = new Date();
return addHeapObject(ret);
};
imports.wbg.__wbg_now_9c5990bda04c7e53 = function() {
const ret = Date.now();
return ret;
};
imports.wbg.__wbg_toISOString_c588641de3e1665d = function(arg0) {
const ret = getObject(arg0).toISOString();
return addHeapObject(ret);
@ -1123,16 +1146,20 @@ function __wbg_get_imports() {
const ret = wasm.memory;
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper2586 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1198, __wbg_adapter_48);
imports.wbg.__wbindgen_closure_wrapper1505 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 752, __wbg_adapter_48);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper2813 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1257, __wbg_adapter_51);
imports.wbg.__wbindgen_closure_wrapper4084 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1975, __wbg_adapter_51);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper2986 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1319, __wbg_adapter_54);
imports.wbg.__wbindgen_closure_wrapper4367 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 2069, __wbg_adapter_54);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper4521 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 2117, __wbg_adapter_57);
return addHeapObject(ret);
};

View file

@ -46,7 +46,7 @@ body {
/* DASHBOARD */
.dash-body {
font-size: .875rem;
font-size: 0.875rem;
}
.feather {
@ -63,14 +63,14 @@ body {
position: fixed;
top: 0;
/* rtl:raw:
right: 0;
*/
right: 0;
*/
bottom: 0;
/* rtl:remove */
left: 0;
z-index: 100; /* Behind the navbar */
padding: 48px 0 0; /* Height of navbar */
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.1);
}
@media (max-width: 767.98px) {
@ -83,7 +83,7 @@ body {
position: relative;
top: 0;
height: calc(100vh - 48px);
padding-top: .5rem;
padding-top: 0.5rem;
overflow-x: hidden;
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
}
@ -108,7 +108,7 @@ body {
}
.sidebar-heading {
font-size: .75rem;
font-size: 0.75rem;
text-transform: uppercase;
}
@ -117,33 +117,33 @@ body {
*/
.navbar-brand {
padding-top: .75rem;
padding-bottom: .75rem;
padding-left: 1.0rem;
padding-right: 2.0rem;
padding-top: 0.75rem;
padding-bottom: 0.75rem;
padding-left: 1rem;
padding-right: 2rem;
font-size: 1.25rem;
}
.navbar .navbar-toggler {
top: .25rem;
top: 0.25rem;
right: 1rem;
}
.navbar .form-control {
padding: .75rem 1rem;
padding: 0.75rem 1rem;
border-width: 0;
border-radius: 0;
}
.form-control-dark {
color: #fff;
background-color: rgba(255, 255, 255, .1);
border-color: rgba(255, 255, 255, .1);
background-color: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.1);
}
.form-control-dark:focus {
border-color: transparent;
box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.25);
}
.vert-center {
@ -153,7 +153,86 @@ body {
}
.kanidm_logo {
width: 12em;
height: 12em;
}
width: 12.0em;
height: 12.0em;
}
.identity-verification-container {
display: flex;
flex-direction: column;
max-width: fit-content;
align-items: center;
margin: auto;
}
:root {
--totp-width-and-height: 30px;
--totp-stroke-width: 60px;
}
.totp-display-container {
padding: 5px 10px;
display: flex;
flex-direction: row;
max-width: fit-content;
align-items: center;
margin: auto;
border-radius: 15px;
background: #21252915;
box-shadow: -5px -5px 11px #ededed, 5px 5px 11px #ffffff;
margin: 15px;
}
.totp-display {
font-size: 35px;
margin: 10px;
}
.totp-timer {
margin: 10px;
position: relative;
height: var(--totp-width-and-height);
width: var(--totp-width-and-height);
}
/* Removes SVG styling that would hide the time label */
.totp-timer__circle {
fill: none;
stroke: none;
}
.totp-timer__path-remaining {
stroke-width: var(--totp-stroke-width);
/* Makes sure the animation starts at the top of the circle */
transform: rotate(90deg);
transform-origin: center;
/* One second aligns with the speed of the countdown timer */
transition: 1s linear all;
stroke: currentColor;
}
.totp-timer__svg {
transform: scaleX(-1);
}
.totp-timer__path-remaining.green {
color: rgb(65, 184, 131);
}
.totp-timer__path-remaining.orange {
color: orange;
}
.totp-timer__path-remaining.red {
color: red;
}
.totp-timer__path-remaining.no-transition {
-webkit-transition: none !important;
-moz-transition: none !important;
-o-transition: none !important;
transition: none !important;
}

View file

@ -156,6 +156,32 @@ impl Component for AdminListAccounts {
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
AdminListAccountsMsg::Responded { response } => {
// TODO: do we paginate here?
/*
// Seems broken
#[cfg(debug_assertions)]
for key in response.keys() {
#[allow(clippy::unwrap_used)]
console::log!(
"response: {:?}",
serde_json::to_string(response.get(key).unwrap()).unwrap()
);
}
*/
self.state = ViewState::Responded { response };
return true;
}
AdminListAccountsMsg::Failed { emsg, kopid } => {
console::log!("emsg: {:?}", emsg);
console::log!("kopid: {:?}", kopid);
}
}
false
}
fn view(&self, _ctx: &Context<Self>) -> Html {
html! {
<>
@ -255,32 +281,6 @@ impl Component for AdminListAccounts {
</>
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
AdminListAccountsMsg::Responded { response } => {
// TODO: do we paginate here?
/*
// Seems broken
#[cfg(debug_assertions)]
for key in response.keys() {
#[allow(clippy::unwrap_used)]
console::log!(
"response: {:?}",
serde_json::to_string(response.get(key).unwrap()).unwrap()
);
}
*/
self.state = ViewState::Responded { response };
return true;
}
AdminListAccountsMsg::Failed { emsg, kopid } => {
console::log!("emsg: {:?}", emsg);
console::log!("kopid: {:?}", kopid);
}
}
false
}
}
// impl AdminListAccounts {

View file

@ -133,19 +133,6 @@ impl Component for AdminListGroups {
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<>
{do_page_header("Group Administration")}
{ alpha_warning_banner() }
<div id={"grouplist"}>
{self.view_state(ctx)}
</div>
</>
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
AdminListGroupsMsg::Responded { response } => {
@ -168,6 +155,19 @@ impl Component for AdminListGroups {
}
false
}
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<>
{do_page_header("Group Administration")}
{ alpha_warning_banner() }
<div id={"grouplist"}>
{self.view_state(ctx)}
</div>
</>
}
}
}
impl AdminListGroups {

View file

@ -130,6 +130,29 @@ impl Component for AdminListOAuth2 {
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
AdminListOAuth2Msg::Responded { response } => {
// TODO: do we paginate here?
#[cfg(debug_assertions)]
for key in response.keys() {
let j = response
.get(key)
.and_then(|k| serde_json::to_string(k).ok())
.unwrap_or_else(|| "Failed to dump response key".to_string());
console::log!("response: {}", j);
}
self.state = ListViewState::Responded { response };
return true;
}
AdminListOAuth2Msg::Failed { emsg, kopid } => {
console::log!("emsg: {:?}", emsg);
console::log!("kopid: {:?}", kopid);
}
}
false
}
fn view(&self, _ctx: &Context<Self>) -> Html {
match &self.state {
ListViewState::Loading => {
@ -223,29 +246,6 @@ impl Component for AdminListOAuth2 {
}
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
AdminListOAuth2Msg::Responded { response } => {
// TODO: do we paginate here?
#[cfg(debug_assertions)]
for key in response.keys() {
let j = response
.get(key)
.and_then(|k| serde_json::to_string(k).ok())
.unwrap_or_else(|| "Failed to dump response key".to_string());
console::log!("response: {}", j);
}
self.state = ListViewState::Responded { response };
return true;
}
AdminListOAuth2Msg::Failed { emsg, kopid } => {
console::log!("emsg: {:?}", emsg);
console::log!("kopid: {:?}", kopid);
}
}
false
}
}
impl From<GetError> for AdminViewOAuth2Msg {

View file

@ -121,6 +121,10 @@ impl Component for ChangeUnixPassword {
}
}
fn changed(&mut self, _ctx: &Context<Self>, _props: &Self::Properties) -> bool {
false
}
fn view(&self, ctx: &Context<Self>) -> Html {
let flash = match &self.state {
State::Error { emsg, kopid } => {
@ -233,10 +237,6 @@ impl Component for ChangeUnixPassword {
}
}
fn changed(&mut self, _ctx: &Context<Self>, _props: &Self::Properties) -> bool {
false
}
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {}
fn destroy(&mut self, _ctx: &Context<Self>) {}

View file

@ -7,6 +7,7 @@ pub mod admin_menu;
pub mod admin_oauth2;
pub mod change_unix_password;
pub mod create_reset_code;
pub mod totpdisplay;
/// creates the "Kanidm is alpha" banner
pub fn alpha_warning_banner() -> Html {

View file

@ -0,0 +1,307 @@
#[cfg(debug_assertions)]
use gloo::console;
use gloo_timers::callback::{Interval, Timeout};
use kanidm_proto::internal::{IdentifyUserRequest, IdentifyUserResponse};
use wasm_bindgen::JsValue;
use wasm_timer::SystemTime;
use yew::prelude::*;
use crate::constants::ID_IDENTITY_VERIFICATION_SYSTEM_TOTP_MODAL;
use crate::views::identityverification::{
IdentifyUserState, IdentifyUserTransition, CORRUPT_STATE_ERROR,
};
use crate::{do_request, utils, RequestMethod};
static DASH_ARRAY_SIZE: u16 = 188;
// Warning occurs at 10s
static WARNING_THRESHOLD: f32 = 0.5;
// Alert occurs at 5s
static ALERT_THRESHOLD: f32 = 0.25;
enum TotpStatus {
Waiting,
Secret(u32),
}
pub struct TotpDisplayApp {
secret: TotpStatus,
step: u32,
main_timer: Option<Timeout>,
ticks_timer: Option<Interval>,
sync_timer: Option<Timeout>,
ticks_left: u8,
}
pub enum Msg {
FetchTotpAndResetTimer,
NewTotp(u32),
Tick,
StartTicking,
TotpConfirmed,
TotpNotConfirmed,
Cancel,
InvalidState,
}
#[derive(Properties, PartialEq)]
pub struct TotpProps {
pub state: IdentifyUserState,
pub other_id: String,
pub cb: Callback<IdentifyUserTransition>,
}
impl TotpDisplayApp {
async fn renew_totp(other_id: String) -> Msg {
let uri = format!("/v1/person/{}/_identify_user", other_id);
let request = IdentifyUserRequest::DisplayCode;
let Ok(state_as_jsvalue) = serde_json::to_string(&request)
.map(|s| JsValue::from(&s))
else {
return Msg::Cancel
};
let response = match do_request(&uri, RequestMethod::POST, Some(state_as_jsvalue)).await {
Ok((_, _, response, _)) => response,
Err(_) => return Msg::Cancel,
};
match serde_wasm_bindgen::from_value(response) {
Ok(IdentifyUserResponse::ProvideCode { totp, step: _ }) => Msg::NewTotp(totp),
_ => Msg::Cancel,
}
}
fn get_time_left_from_now(&self) -> u32 {
#[allow(clippy::expect_used)]
let dur = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("invalid duration from epoch now");
let secs: u128 = dur.as_millis();
let step = self.step as u128;
(step * 1000 - secs % (step * 1000)) as u32
}
fn get_time_left_from_now_selfless(step: u128) -> u32 {
#[allow(clippy::expect_used)]
let dur = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("invalid duration from epoch now");
let secs: u128 = dur.as_millis();
(step * 1000 - secs % (step * 1000)) as u32
}
fn get_ring_color(&self, time_remaining: u32) -> AttrValue {
// it's a bit hacky but we want it to be green starting from 0 (aka no totp ring) so by the next second
// (aka when it goes to 30) it's already 100% green, since the transition takes 1s we have to start doing this from
// time_remaining == 1
AttrValue::from(if time_remaining <= 1 {
"green"
} else if time_remaining <= (ALERT_THRESHOLD * self.step as f32) as u32 {
"red"
} else if time_remaining <= (WARNING_THRESHOLD * self.step as f32) as u32 {
"orange"
} else {
"green"
})
}
}
impl Component for TotpDisplayApp {
type Message = Msg;
type Properties = TotpProps;
fn create(ctx: &Context<Self>) -> Self {
#[cfg(debug_assertions)]
console::debug!("totp modal create");
ctx.link().send_message(Msg::FetchTotpAndResetTimer);
let (totp, step) = match &ctx.props().state {
IdentifyUserState::DisplayCodeFirst { self_totp, step }
| IdentifyUserState::DisplayCodeSecond { self_totp, step } => (self_totp, step),
_ => {
ctx.link().send_message(Msg::InvalidState);
(&0, &0)
}
};
let time_left = Self::get_time_left_from_now_selfless(*step as u128);
let handle = {
let link = ctx.link().clone();
Timeout::new(time_left, move || {
link.send_message(Msg::FetchTotpAndResetTimer)
})
};
ctx.link().send_message(Msg::NewTotp(*totp));
TotpDisplayApp {
secret: TotpStatus::Waiting,
main_timer: Some(handle),
ticks_left: 30u8,
ticks_timer: None,
sync_timer: None,
step: *step,
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
#[cfg(debug_assertions)]
console::debug!("totp display::update");
match msg {
Msg::FetchTotpAndResetTimer => {
self.secret = TotpStatus::Waiting;
let time_left = self.get_time_left_from_now();
let handle = {
let link = ctx.link().clone();
Timeout::new(time_left, move || {
link.send_message(Msg::FetchTotpAndResetTimer)
})
};
ctx.link()
.send_future(Self::renew_totp(ctx.props().other_id.clone()));
self.main_timer = Some(handle);
}
Msg::NewTotp(totp) => {
// once we get the new totp we update it and we call start_ticking on the next
// even second
let millis_to_next_second = self.get_time_left_from_now() % 1000;
self.secret = TotpStatus::Secret(totp);
let link = ctx.link().clone();
self.sync_timer = Some(Timeout::new(millis_to_next_second, move || {
link.send_message(Msg::StartTicking)
}));
}
Msg::StartTicking => {
self.ticks_left = (self.get_time_left_from_now() / 1000) as u8 + 1;
self.ticks_timer = {
let link = ctx.link().clone();
Some(Interval::new(1000, move || link.send_message(Msg::Tick)))
};
}
Msg::Tick => {
// if the ticks are less than 0 it means the other timeout is also 0 and therefore
// it will send a FetchTotpAndResetTimer message so we don't need to worry about that here
if self.ticks_left > 0 {
self.ticks_left -= 1;
}
}
Msg::Cancel => {
self.main_timer = None;
self.ticks_left = 100;
}
Msg::TotpConfirmed => {
utils::modal_hide_by_id(ID_IDENTITY_VERIFICATION_SYSTEM_TOTP_MODAL);
if let IdentifyUserState::DisplayCodeFirst { .. } = &ctx.props().state {
ctx.props().cb.emit(IdentifyUserTransition::WaitForCode)
} else {
ctx.props().cb.emit(IdentifyUserTransition::Success)
}
}
Msg::TotpNotConfirmed => {
utils::modal_hide_by_id(ID_IDENTITY_VERIFICATION_SYSTEM_TOTP_MODAL);
}
Msg::InvalidState => {
utils::modal_hide_by_id(ID_IDENTITY_VERIFICATION_SYSTEM_TOTP_MODAL);
ctx.props().cb.emit(IdentifyUserTransition::Error {
msg: CORRUPT_STATE_ERROR.to_string(),
})
}
}
true
}
fn changed(&mut self, _ctx: &Context<Self>, _props: &Self::Properties) -> bool {
#[cfg(debug_assertions)]
console::debug!("totp modal::change");
false
}
fn view(&self, ctx: &Context<Self>) -> Html {
#[cfg(debug_assertions)]
console::debug!("totp modal::view");
#[cfg(debug_assertions)]
console::debug!(self.ticks_left);
let step = self.step;
let time_fraction = self.ticks_left as f32 / step as f32;
let color_class = self.get_ring_color(self.ticks_left as u32);
// at the first tick we remove the no-transition class if present!
let classes = format!("totp-timer__path-remaining {}", color_class);
let shortened_time_fraction = time_fraction - (1.0 / step as f32) * (1.0 - time_fraction);
let attr_value = AttrValue::from(format!(
"{} {}",
shortened_time_fraction * DASH_ARRAY_SIZE as f32,
DASH_ARRAY_SIZE
));
let other_id = ctx.props().other_id.clone();
html! {
<>
<div class="identity-verification-container">
<div class="totp-display-container">
{ if let TotpStatus::Secret (totp) = &self.secret {
html!{
<span class="totp-display">
<b>{ totp } </b>
</span>
}
} else {
html! {
<p>
{ "Fetching your totp..." }
</p>
}
} }
<div class="totp-timer">
<svg class="totp-timer__svg" viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg">
<g class="totp-timer__circle">
<path
id="totp-timer-path-remaining"
stroke-dasharray={attr_value}
class={classes!(classes)}
d="
M 60, 60
m -30, 0
a 30,30 0 1,0 60,0
a 30,30 0 1,0 -60,0
"
></path>
</g>
</svg>
</div>
</div>
<button
class="btn btn-secondary"
data-bs-toggle="modal"
data-bs-target={format!("#{}", ID_IDENTITY_VERIFICATION_SYSTEM_TOTP_MODAL)}
>{" Continue "}</button>
</div>
<div class="modal fade" id={ID_IDENTITY_VERIFICATION_SYSTEM_TOTP_MODAL} tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-body">
{"Did you confirm that "} {other_id} {" correctly verified your code? If you proceed, you won't be able to go back."}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" onclick={ ctx.link()
.callback(move |_| Msg::TotpNotConfirmed)} >{"Go back"}</button>
<button type="button" class="btn btn-secondary" onclick={ ctx.link()
.callback(move |_| Msg::TotpConfirmed)}>{"Continue"}</button>
</div>
</div>
</div>
</div>
</>
}
}
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
#[cfg(debug_assertions)]
console::debug!("totp modal::rendered");
}
fn destroy(&mut self, _ctx: &Context<Self>) {
#[cfg(debug_assertions)]
console::debug!("totp modal::destroy");
}
}

View file

@ -13,6 +13,7 @@ pub const ID_SIGNOUTMODAL: &str = "signoutModal";
// the HTML element ID that the unix password dialog box has
pub const ID_UNIX_PASSWORDCHANGE: &str = "unixPasswordModal";
pub const ID_IDENTITY_VERIFICATION_SYSTEM_TOTP_MODAL: &str = "identityVerificationSystemTotpModal";
pub const ID_CRED_RESET_CODE: &str = "credResetCodeModal";
// classes for buttons
pub const CLASS_BUTTON_DARK: &str = "btn btn-dark";

View file

@ -81,12 +81,6 @@ impl Component for DeleteApp {
DeleteApp { state: State::Init }
}
fn changed(&mut self, _ctx: &Context<Self>, _props: &Self::Properties) -> bool {
#[cfg(debug_assertions)]
console::debug!("delete modal::change");
false
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
#[cfg(debug_assertions)]
console::debug!("delete modal::update");
@ -118,14 +112,10 @@ impl Component for DeleteApp {
true
}
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
fn changed(&mut self, _ctx: &Context<Self>, _props: &Self::Properties) -> bool {
#[cfg(debug_assertions)]
console::debug!("delete modal::rendered");
}
fn destroy(&mut self, _ctx: &Context<Self>) {
#[cfg(debug_assertions)]
console::debug!("delete modal::destroy");
console::debug!("delete modal::change");
false
}
fn view(&self, ctx: &Context<Self>) -> Html {
@ -179,4 +169,14 @@ impl Component for DeleteApp {
</div>
}
}
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
#[cfg(debug_assertions)]
console::debug!("delete modal::rendered");
}
fn destroy(&mut self, _ctx: &Context<Self>) {
#[cfg(debug_assertions)]
console::debug!("delete modal::destroy");
}
}

View file

@ -106,11 +106,6 @@ impl Component for PasskeyModalApp {
}
}
fn changed(&mut self, _ctx: &Context<Self>, _props: &Self::Properties) -> bool {
console::debug!("passkey modal::change");
false
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
console::debug!("passkey modal::update");
let cb = ctx.props().cb.clone();
@ -230,12 +225,9 @@ impl Component for PasskeyModalApp {
true
}
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
console::debug!("passkey modal::rendered");
}
fn destroy(&mut self, _ctx: &Context<Self>) {
console::debug!("passkey modal::destroy");
fn changed(&mut self, _ctx: &Context<Self>, _props: &Self::Properties) -> bool {
console::debug!("passkey modal::change");
false
}
fn view(&self, ctx: &Context<Self>) -> Html {
@ -378,4 +370,12 @@ impl Component for PasskeyModalApp {
</>
}
}
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
console::debug!("passkey modal::rendered");
}
fn destroy(&mut self, _ctx: &Context<Self>) {
console::debug!("passkey modal::destroy");
}
}

View file

@ -107,12 +107,6 @@ impl Component for PwModalApp {
}
}
fn changed(&mut self, _ctx: &Context<Self>, _props: &Self::Properties) -> bool {
#[cfg(debug_assertions)]
console::debug!("pw modal::change");
false
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
#[cfg(debug_assertions)]
console::debug!("pw modal::update");
@ -166,14 +160,10 @@ impl Component for PwModalApp {
true
}
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
fn changed(&mut self, _ctx: &Context<Self>, _props: &Self::Properties) -> bool {
#[cfg(debug_assertions)]
console::debug!("pw modal::rendered");
}
fn destroy(&mut self, _ctx: &Context<Self>) {
#[cfg(debug_assertions)]
console::debug!("pw modal::destroy");
console::debug!("pw modal::change");
false
}
fn view(&self, ctx: &Context<Self>) -> Html {
@ -315,4 +305,14 @@ impl Component for PwModalApp {
</div>
}
}
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
#[cfg(debug_assertions)]
console::debug!("pw modal::rendered");
}
fn destroy(&mut self, _ctx: &Context<Self>) {
#[cfg(debug_assertions)]
console::debug!("pw modal::destroy");
}
}

View file

@ -120,12 +120,6 @@ impl Component for TotpModalApp {
}
}
fn changed(&mut self, _ctx: &Context<Self>, _props: &Self::Properties) -> bool {
#[cfg(debug_assertions)]
console::debug!("totp modal::change");
false
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
#[cfg(debug_assertions)]
console::debug!("totp modal::update");
@ -226,14 +220,10 @@ impl Component for TotpModalApp {
true
}
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
fn changed(&mut self, _ctx: &Context<Self>, _props: &Self::Properties) -> bool {
#[cfg(debug_assertions)]
console::debug!("totp modal::rendered");
}
fn destroy(&mut self, _ctx: &Context<Self>) {
#[cfg(debug_assertions)]
console::debug!("totp modal::destroy");
console::debug!("totp modal::change");
false
}
fn view(&self, ctx: &Context<Self>) -> Html {
@ -445,4 +435,14 @@ impl Component for TotpModalApp {
</>
}
}
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
#[cfg(debug_assertions)]
console::debug!("totp modal::rendered");
}
fn destroy(&mut self, _ctx: &Context<Self>) {
#[cfg(debug_assertions)]
console::debug!("totp modal::destroy");
}
}

View file

@ -0,0 +1,529 @@
#[cfg(debug_assertions)]
use gloo::console;
use kanidm_proto::internal::{IdentifyUserRequest, IdentifyUserResponse};
use regex::Regex;
use wasm_bindgen::JsValue;
use yew::prelude::*;
use crate::components::totpdisplay::TotpDisplayApp;
use crate::constants::{CLASS_DIV_LOGIN_BUTTON, CLASS_DIV_LOGIN_FIELD};
use crate::error::FetchError;
use crate::utils::{self};
use crate::views::ViewProps;
use crate::{do_request, RequestMethod};
#[derive(Clone, Debug, PartialEq)]
pub enum IdentifyUserState {
Start,
IdDisplayAndSubmit,
SubmitCodeFirst {
other_totp: Option<u32>,
totp_valid: bool,
},
SubmitCodeSecond {
other_totp: Option<u32>,
totp_valid: bool,
},
DisplayCodeFirst {
self_totp: u32,
step: u32,
},
DisplayCodeSecond {
self_totp: u32,
step: u32,
},
Success,
Error {
msg: String,
},
}
#[derive(Clone, Debug, PartialEq)]
pub enum IdentifyUserTransition {
UpdateSelfIdentity { spn: String },
IdentityVerificationAvailable,
ProvideCode { totp: u32, step: u32 },
WaitForCode,
Success,
Error { msg: String },
CheckInput { input: String },
DoNothing,
}
pub(crate) static CORRUPT_STATE_ERROR: &str =
"The identity verification flow is in a corrupt state, please abort and start again";
static UNAVAILABLE_IDENTITY_VERIFICATION_ERROR: &str =
"The identity verification feature is currently unavailable for this account 😢";
static INVALID_USERID_ERROR: &str = "The provided UserID is invalid!";
lazy_static::lazy_static! {
pub static ref VALIDATE_TOTP_RE: Regex = {
#[allow(clippy::expect_used)]
Regex::new(r"^\d{6}$").expect("Invalid singleline regex found")
};
}
#[derive(Clone)]
pub struct IdentityVerificationApp {
other_id: String,
self_id: String,
state: IdentifyUserState,
cb: Callback<IdentifyUserTransition>,
}
impl From<FetchError> for IdentifyUserTransition {
fn from(value: FetchError) -> Self {
IdentifyUserTransition::Error {
msg: value.to_string(),
}
}
}
impl From<IdentifyUserResponse> for IdentifyUserTransition {
fn from(value: IdentifyUserResponse) -> Self {
match value {
IdentifyUserResponse::IdentityVerificationUnavailable => {
IdentifyUserTransition::Error {
msg: UNAVAILABLE_IDENTITY_VERIFICATION_ERROR.to_string(),
}
}
IdentifyUserResponse::IdentityVerificationAvailable => {
IdentifyUserTransition::IdentityVerificationAvailable
}
IdentifyUserResponse::ProvideCode { totp, step } => {
IdentifyUserTransition::ProvideCode { totp, step }
}
IdentifyUserResponse::WaitForCode => IdentifyUserTransition::WaitForCode,
IdentifyUserResponse::Success => IdentifyUserTransition::Success,
IdentifyUserResponse::CodeFailure => IdentifyUserTransition::Error {
msg: "The code provided does not belong to the given user!".to_string(),
},
IdentifyUserResponse::InvalidUserId => IdentifyUserTransition::Error {
msg: INVALID_USERID_ERROR.to_string(),
},
}
}
}
impl Component for IdentityVerificationApp {
type Message = IdentifyUserTransition;
type Properties = ViewProps;
fn create(ctx: &Context<Self>) -> Self {
#[cfg(debug_assertions)]
console::debug!("views::identity-verification::create");
let id = Self::get_id(ctx);
let state = IdentifyUserState::Start;
ctx.link().send_future(Self::get_transition_from_start(
state.to_owned(),
id.clone(),
));
ctx.link().send_future(Self::update_self_id(id.clone()));
let cb = Callback::from({
let link = ctx.link().clone();
move |identify_user_transition| {
link.send_message(identify_user_transition);
}
});
IdentityVerificationApp {
state,
cb,
other_id: String::new(),
self_id: id.clone(),
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
#[cfg(debug_assertions)]
console::debug!("views::identity-verification::update");
match msg {
IdentifyUserTransition::UpdateSelfIdentity { spn } => {
#[cfg(debug_assertions)]
console::debug!("identity-verification update self identity: {}", &spn);
self.self_id = spn;
}
IdentifyUserTransition::IdentityVerificationAvailable => {
if matches!(self.state, IdentifyUserState::Start) {
self.state = IdentifyUserState::IdDisplayAndSubmit
} else {
self.set_state_to_corrupt_state_err()
}
// here the only thing to do is to display the page where the user can insert the ID
}
IdentifyUserTransition::ProvideCode { totp, step } => {
// here we have two possibilities: we come from the 'IdDisplayAndSubmit' and therefore
// we go into DisplayCodeFirst, or we come from SubmitCodeFirst and therefore we go to DisplayCodeSecond
match &self.state {
IdentifyUserState::IdDisplayAndSubmit => {
self.state = IdentifyUserState::DisplayCodeFirst {
self_totp: totp,
step,
};
}
IdentifyUserState::SubmitCodeFirst { .. } => {
self.state = IdentifyUserState::DisplayCodeSecond {
self_totp: totp,
step,
};
}
_ => self.set_state_to_corrupt_state_err(),
}
}
IdentifyUserTransition::WaitForCode => {
// here again we have two possibilities: we either come from IdDisplayAndSubmit or from DisplayCodeFirst
// if we are in the first case then we go to SubmitCodeFirst, otherwise we go to SubmitCodeSecond
match &self.state {
IdentifyUserState::IdDisplayAndSubmit => {
self.state = IdentifyUserState::SubmitCodeFirst {
other_totp: None,
totp_valid: false,
};
}
IdentifyUserState::DisplayCodeFirst { .. } => {
self.state = IdentifyUserState::SubmitCodeSecond {
other_totp: None,
totp_valid: false,
};
}
_ => self.set_state_to_corrupt_state_err(),
}
}
IdentifyUserTransition::Success => match self.state {
IdentifyUserState::DisplayCodeSecond { .. }
| IdentifyUserState::SubmitCodeSecond { .. } => {
self.state = IdentifyUserState::Success;
}
_ => self.set_state_to_corrupt_state_err(),
},
IdentifyUserTransition::DoNothing => return false,
IdentifyUserTransition::CheckInput { input } => {
// according to our beautiful state machine if CheckInput was called we must be in either IdDisplayAndSubmit, SubmitCodeFirst or SubmitCodeSecond.
// if that's the case then we just update the valid_status accordingly
// If we're in another state we'll land in the infamous invalid flow error!
match &mut self.state {
IdentifyUserState::IdDisplayAndSubmit => self.other_id = input,
IdentifyUserState::SubmitCodeFirst {
other_totp,
totp_valid,
}
| IdentifyUserState::SubmitCodeSecond {
other_totp,
totp_valid,
} => {
*totp_valid = VALIDATE_TOTP_RE.is_match(&input);
#[cfg(debug_assertions)]
console::debug!(input.clone());
*other_totp = input.parse::<u32>().ok();
}
_ => self.set_state_to_corrupt_state_err(),
}
}
IdentifyUserTransition::Error { msg } => self.state = IdentifyUserState::Error { msg },
}
true
}
fn changed(&mut self, _ctx: &Context<Self>, _props: &Self::Properties) -> bool {
#[cfg(debug_assertions)]
console::debug!("views::identity-verification::changed");
false
}
fn view(&self, ctx: &Context<Self>) -> Html {
match &self.state {
IdentifyUserState::Start => self.view_start(),
IdentifyUserState::IdDisplayAndSubmit => self.view_id_submit_and_display(ctx),
IdentifyUserState::SubmitCodeFirst {
other_totp,
totp_valid,
}
| IdentifyUserState::SubmitCodeSecond {
other_totp,
totp_valid,
} => self.view_submit_code(ctx, *other_totp, *totp_valid),
IdentifyUserState::DisplayCodeFirst { .. }
| IdentifyUserState::DisplayCodeSecond { .. } => self.view_display_code(ctx),
IdentifyUserState::Success => self.view_success(),
IdentifyUserState::Error { msg } => self.view_error(msg),
}
}
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
#[cfg(debug_assertions)]
console::debug!("views::apps::rendered");
}
}
impl IdentityVerificationApp {
fn view_start(&self) -> Html {
html! {
<>
<div class="vert-center">
<div class="spinner-border text-dark" role="status">
<span class="visually-hidden">{ "Loading..." }</span>
</div>
</div>
</>
}
}
fn view_id_submit_and_display(&self, ctx: &Context<Self>) -> Html {
let self_clone = self.clone();
let other_id = || self.other_id.clone();
html! {
<div class="identity-verification-container">
<div class="container">
<p>{ "When asked for your ID, provide the following: "} <b>{ self.self_id.to_string() } </b></p>
</div>
<div class="container">
<hr/>
</div>
<div class="container">
<label for="ID" class="form-label"> {"Ask for the other person's ID, and insert it here:
"}</label>
<form
onsubmit={ ctx.link().callback_future(move |e: SubmitEvent| {
#[cfg(debug_assertions)]
console::debug!("identity-verification::view_state -> Init - prevent_default()".to_string());
e.prevent_default();
self_clone.to_owned().get_transition_from_id_display_and_submit()
} ) }
>
<div class={CLASS_DIV_LOGIN_FIELD}>
<input
autofocus=true
class="autofocus form-control"
id="other-user-id-input"
name="other-user-id-input"
type="text"
oninput={Self::input_callback(ctx, "other-user-id-input")}
autocomplete="ID"
value={ other_id() }
/>
</div>
<div class={CLASS_DIV_LOGIN_BUTTON}>
<button
type="submit"
class="btn btn-primary"
>{" Continue "}</button>
</div>
</form>
</div>
</div>
}
}
fn view_display_code(&self, _ctx: &Context<Self>) -> Html {
let other_id = self.other_id.clone();
html! {
<div class="identity-verification-container">
<div class="container">
<p>{ "Please provide the following code when asked!"} <b> </b></p>
<TotpDisplayApp other_id = {other_id} state = { self.state.clone() } cb = {self.cb.clone()} />
</div>
</div>
}
}
fn view_submit_code(&self, ctx: &Context<Self>, totp: Option<u32>, totp_valid: bool) -> Html {
let self_clone = self.clone();
html! {
<div class="identity-verification-container">
<div class="container">
<label for="ID" class="form-label"> {"Ask for "} { self.other_id.clone() }{ "'s code, and insert it here:"}</label>
<form
onsubmit={ ctx.link().callback_future(move |e: SubmitEvent| {
#[cfg(debug_assertions)]
console::debug!("identity-verification::view_state -> Init - prevent_default()".to_string());
e.prevent_default();
self_clone.to_owned().get_transition_from_submit_code()
} ) }
>
<div class={CLASS_DIV_LOGIN_FIELD}>
<input
autofocus=true
class="autofocus form-control"
id="totp-code-input"
name="code"
type="number"
step="1"
max="999999"
min="0"
autocomplete="code"
oninput={Self::input_callback(ctx, "totp-code-input")}
value={ totp.map(|x| x.to_string()) }
/>
</div>
<div class={CLASS_DIV_LOGIN_BUTTON}>
<button
type="submit"
class="btn btn-primary"
disabled={ !totp_valid }
>{" Continue "}</button>
</div>
</form>
</div>
</div>
}
}
fn view_error(&self, error_message: &str) -> Html {
html! {
<>
<p class="text-center">
<img src="/pkg/img/logo-square.svg" alt="Kanidm" class="kanidm_logo"/>
</p>
<div class="alert alert-danger" role="alert">
<h2>{ "An Error Occurred 🥺" }</h2>
<p>{ error_message }</p>
</div>
<p class="text-center">
<a href="/"><button href="/" class="btn btn-secondary" aria-label="Return home">{"Return to the home page"}</button></a>
</p>
</>
}
}
fn view_success(&self) -> Html {
let other_id = self.other_id.clone();
html! {
<>
<div class="alert alert-success" role="alert">
<h4 class="alert-heading">{"Success 🎉🎉"}</h4>
<p><b>{other_id}</b>{"'s identity has been successfully confirmed!"}</p>
</div>
<p class="text-center">
<a href="/"><button href="/" class="btn btn-secondary" aria-label="Return home">{"Return to the home page"}</button></a>
</p>
</>
}
}
// the purpose of the following functions is to get what to do next in the state machine.
// each main view has its own function, that is the start view (even though it's displayed for few ms), the id_display_and_submit view
// and the submit_code
// we have to prefix
async fn get_transition_from_start(
_state: IdentifyUserState,
self_id: String,
) -> IdentifyUserTransition {
#[cfg(debug_assertions)]
assert!(matches!(_state, IdentifyUserState::Start));
// IdentifyUserRequest is hard coded as this function is called on start so that's the only possible state
let response = match Self::do_typed_request(IdentifyUserRequest::Start, &self_id).await {
Ok(res) => res,
Err(s) => return IdentifyUserTransition::Error { msg: s.to_string() },
};
IdentifyUserTransition::from(response)
}
async fn get_transition_from_id_display_and_submit(self) -> IdentifyUserTransition {
let request = match &self.state {
IdentifyUserState::IdDisplayAndSubmit => IdentifyUserRequest::Start,
_ => {
return IdentifyUserTransition::Error {
msg: CORRUPT_STATE_ERROR.to_string(),
}
}
};
let response = match Self::do_typed_request(request, &self.other_id).await {
Ok(res) => res,
Err(s) => return IdentifyUserTransition::Error { msg: s.to_string() },
};
IdentifyUserTransition::from(response)
}
async fn get_transition_from_submit_code(self) -> IdentifyUserTransition {
let request = match &self.state {
IdentifyUserState::SubmitCodeFirst {
other_totp,
totp_valid,
}
| IdentifyUserState::SubmitCodeSecond {
other_totp,
totp_valid,
// in no case this function should have been called with an invalid code, but if that's the case then we do nothing
} => {
if *totp_valid {
IdentifyUserRequest::SubmitCode {
other_totp: other_totp.unwrap_or_default(), // we know that the totp is valid so this should always be Some,
// if for some reason it's None then we are still covered
}
} else {
return IdentifyUserTransition::DoNothing;
}
}
_ => {
return IdentifyUserTransition::Error {
msg: CORRUPT_STATE_ERROR.to_string(),
}
}
};
let response = match Self::do_typed_request(request, &self.other_id).await {
Ok(res) => res,
Err(s) => return IdentifyUserTransition::Error { msg: s.to_string() },
};
IdentifyUserTransition::from(response)
}
async fn do_typed_request(
request: IdentifyUserRequest,
other_id: &str,
) -> Result<IdentifyUserResponse, String> {
let uri = format!("/v1/person/{}/_identify_user", other_id);
let request_as_jsvalue = serde_json::to_string(&request)
.map(|s| JsValue::from(&s))
.map_err(|_| "Invalid request!".to_string())?;
let (_, status, response, _) =
do_request(&uri, RequestMethod::POST, Some(request_as_jsvalue))
.await
.map_err(|e| e.to_string())?;
if status != 200 {
Err(format!(
"The server responded with status code {status}, here is what went wrong: {}",
response.as_string().unwrap_or_default()
))
} else {
serde_wasm_bindgen::from_value(response)
.map_err(|_| "Invalid response from server!".to_string())
}
}
async fn update_self_id(uuid: String) -> IdentifyUserTransition {
let uri = format!("/v1/person/{}/_attr/spn", uuid);
let outcome: Option<Vec<String>> =
match do_request(&uri, RequestMethod::GET, None).await.ok() {
None => None,
Some((_, _, res, ..)) => serde_wasm_bindgen::from_value(res).ok(),
};
match outcome.and_then(|v| v.first().cloned()) {
Some(spn) => IdentifyUserTransition::UpdateSelfIdentity { spn },
None => IdentifyUserTransition::DoNothing,
}
}
fn get_id(ctx: &Context<Self>) -> String {
let uat = &ctx.props().current_user_uat;
uat.uuid.to_string()
}
fn input_callback(ctx: &Context<Self>, element_id: &str) -> yew::Callback<web_sys::InputEvent> {
let cloned_element_id = element_id.to_string();
ctx.link().callback(move |_| {
let input = utils::get_value_from_element_id(&cloned_element_id).unwrap_or_default();
IdentifyUserTransition::CheckInput { input }
})
}
fn set_state_to_corrupt_state_err(&mut self) {
self.state = IdentifyUserState::Error {
msg: CORRUPT_STATE_ERROR.to_string(),
}
}
}

View file

@ -11,9 +11,11 @@ use crate::models;
use crate::{do_request, error::*, RequestMethod};
mod apps;
pub mod identityverification;
mod profile;
use apps::AppsApp;
use identityverification::IdentityVerificationApp;
use profile::ProfileApp;
#[derive(Routable, PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
@ -27,6 +29,9 @@ pub enum ViewRoute {
#[at("/ui/profile")]
Profile,
#[at("/ui/identity-verification")]
IdentityVerification,
#[not_found]
#[at("/ui/404")]
NotFound,
@ -117,12 +122,6 @@ impl Component for ViewsApp {
ViewsApp { state }
}
fn changed(&mut self, _ctx: &Context<Self>, _props: &Self::Properties) -> bool {
#[cfg(debug_assertions)]
console::debug!("views::changed");
false
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
#[cfg(debug_assertions)]
console::debug!("views::update");
@ -162,9 +161,10 @@ impl Component for ViewsApp {
}
}
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
fn changed(&mut self, _ctx: &Context<Self>, _props: &Self::Properties) -> bool {
#[cfg(debug_assertions)]
console::debug!("views::rendered");
console::debug!("views::changed");
false
}
fn view(&self, ctx: &Context<Self>) -> Html {
@ -217,14 +217,19 @@ impl Component for ViewsApp {
}
}
}
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
#[cfg(debug_assertions)]
console::debug!("views::rendered");
}
}
impl ViewsApp {
/// The base page for the user dashboard
fn view_authenticated(&self, ctx: &Context<Self>, uat: &UserAuthToken) -> Html {
let current_user_uat = uat.clone();
let ui_hint_experimental = uat.ui_hints.contains(&UiHint::ExperimentalFeatures);
let credential_update = uat.ui_hints.contains(&UiHint::CredentialUpdate);
// WARN set dash-body against body here?
html! {
@ -247,13 +252,22 @@ impl ViewsApp {
{ "Apps" }
</Link<ViewRoute>>
</li>
<li class="mb-1">
if ui_hint_experimental {
<li class="mb-1">
<Link<ViewRoute> classes="nav-link" to={ViewRoute::IdentityVerification}>
<span data-feather="file"></span>
{ "Identity verification" }
</Link<ViewRoute>>
</li>
}
if credential_update {
<li class="mb-1">
<Link<ViewRoute> classes="nav-link" to={ViewRoute::Profile}>
<span data-feather="file"></span>
{ "Profile" }
</Link<ViewRoute>>
</li>
</li>
}
if ui_hint_experimental {
<li class="mb-1">
@ -309,6 +323,7 @@ impl ViewsApp {
<Switch<AdminRoute> render={ admin_routes } />
},
#[allow(clippy::let_unit_value)]
ViewRoute::IdentityVerification => html! { <IdentityVerificationApp current_user_uat={ current_user_uat.clone() } />},
ViewRoute::Apps => html! { <AppsApp /> },
ViewRoute::Profile => html! { <ProfileApp current_user_uat={ current_user_uat.clone() } /> },
ViewRoute::NotFound => html! {

View file

@ -59,12 +59,6 @@ impl Component for ProfileApp {
ProfileApp { state: State::Init }
}
fn changed(&mut self, _ctx: &Context<Self>, _props: &Self::Properties) -> bool {
#[cfg(debug_assertions)]
console::debug!("views::security::changed");
true
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
#[cfg(debug_assertions)]
console::debug!("views::security::update");
@ -124,9 +118,10 @@ impl Component for ProfileApp {
}
}
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
fn changed(&mut self, _ctx: &Context<Self>, _props: &Self::Properties) -> bool {
#[cfg(debug_assertions)]
console::debug!("views::security::rendered");
console::debug!("views::security::changed");
true
}
fn view(&self, ctx: &Context<Self>) -> Html {
@ -191,6 +186,11 @@ impl Component for ProfileApp {
</>
}
}
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
#[cfg(debug_assertions)]
console::debug!("views::security::rendered");
}
}
impl ProfileApp {

View file

@ -46,7 +46,7 @@ body {
/* DASHBOARD */
.dash-body {
font-size: .875rem;
font-size: 0.875rem;
}
.feather {
@ -63,14 +63,14 @@ body {
position: fixed;
top: 0;
/* rtl:raw:
right: 0;
*/
right: 0;
*/
bottom: 0;
/* rtl:remove */
left: 0;
z-index: 100; /* Behind the navbar */
padding: 48px 0 0; /* Height of navbar */
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.1);
}
@media (max-width: 767.98px) {
@ -83,7 +83,7 @@ body {
position: relative;
top: 0;
height: calc(100vh - 48px);
padding-top: .5rem;
padding-top: 0.5rem;
overflow-x: hidden;
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
}
@ -108,7 +108,7 @@ body {
}
.sidebar-heading {
font-size: .75rem;
font-size: 0.75rem;
text-transform: uppercase;
}
@ -117,33 +117,33 @@ body {
*/
.navbar-brand {
padding-top: .75rem;
padding-bottom: .75rem;
padding-left: 1.0rem;
padding-right: 2.0rem;
padding-top: 0.75rem;
padding-bottom: 0.75rem;
padding-left: 1rem;
padding-right: 2rem;
font-size: 1.25rem;
}
.navbar .navbar-toggler {
top: .25rem;
top: 0.25rem;
right: 1rem;
}
.navbar .form-control {
padding: .75rem 1rem;
padding: 0.75rem 1rem;
border-width: 0;
border-radius: 0;
}
.form-control-dark {
color: #fff;
background-color: rgba(255, 255, 255, .1);
border-color: rgba(255, 255, 255, .1);
background-color: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.1);
}
.form-control-dark:focus {
border-color: transparent;
box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.25);
}
.vert-center {
@ -153,7 +153,86 @@ body {
}
.kanidm_logo {
width: 12em;
height: 12em;
}
width: 12.0em;
height: 12.0em;
}
.identity-verification-container {
display: flex;
flex-direction: column;
max-width: fit-content;
align-items: center;
margin: auto;
}
:root {
--totp-width-and-height: 30px;
--totp-stroke-width: 60px;
}
.totp-display-container {
padding: 5px 10px;
display: flex;
flex-direction: row;
max-width: fit-content;
align-items: center;
margin: auto;
border-radius: 15px;
background: #21252915;
box-shadow: -5px -5px 11px #ededed, 5px 5px 11px #ffffff;
margin: 15px;
}
.totp-display {
font-size: 35px;
margin: 10px;
}
.totp-timer {
margin: 10px;
position: relative;
height: var(--totp-width-and-height);
width: var(--totp-width-and-height);
}
/* Removes SVG styling that would hide the time label */
.totp-timer__circle {
fill: none;
stroke: none;
}
.totp-timer__path-remaining {
stroke-width: var(--totp-stroke-width);
/* Makes sure the animation starts at the top of the circle */
transform: rotate(90deg);
transform-origin: center;
/* One second aligns with the speed of the countdown timer */
transition: 1s linear all;
stroke: currentColor;
}
.totp-timer__svg {
transform: scaleX(-1);
}
.totp-timer__path-remaining.green {
color: rgb(65, 184, 131);
}
.totp-timer__path-remaining.orange {
color: orange;
}
.totp-timer__path-remaining.red {
color: red;
}
.totp-timer__path-remaining.no-transition {
-webkit-transition: none !important;
-moz-transition: none !important;
-o-transition: none !important;
transition: none !important;
}