mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
Identity verification feature (#1819)
This commit is contained in:
parent
87866c568b
commit
003234c2d0
58
Cargo.lock
generated
58
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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(<e)
|
||||
}
|
||||
|
||||
#[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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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")),
|
||||
|
|
|
@ -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()
|
||||
};
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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> {
|
||||
|
|
650
server/lib/src/idm/identityverification.rs
Normal file
650
server/lib/src/idm/identityverification.rs
Normal 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(¤t_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"))
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
284
server/lib/src/plugins/eckeygen.rs
Normal file
284
server/lib/src/plugins/eckeygen.rs
Normal 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")
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -390,6 +390,9 @@ pub enum ReplAttrV1 {
|
|||
AuditLogString {
|
||||
set: Vec<(Cid, String)>,
|
||||
},
|
||||
EcKeyPrivate {
|
||||
key: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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())];
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
218
server/lib/src/valueset/eckey.rs
Normal file
218
server/lib/src/valueset/eckey.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
321
server/testkit/tests/identity_verification_tests.rs
Normal file
321
server/testkit/tests/identity_verification_tests.rs
Normal 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());
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>) {}
|
||||
|
|
|
@ -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 {
|
||||
|
|
307
server/web_ui/src/components/totpdisplay.rs
Normal file
307
server/web_ui/src/components/totpdisplay.rs
Normal 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");
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
529
server/web_ui/src/views/identityverification.rs
Normal file
529
server/web_ui/src/views/identityverification.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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! {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue