mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
idv cli (#2001)
This commit is contained in:
parent
753ef82a4b
commit
70b19f0630
180
Cargo.lock
generated
180
Cargo.lock
generated
|
@ -35,6 +35,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
]
|
||||
|
@ -973,6 +974,31 @@ dependencies = [
|
|||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"crossterm_winapi",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot 0.12.1",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
|
@ -1004,6 +1030,45 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cursive"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5438eb16bdd8af51b31e74764fef5d0a9260227a5ec82ba75c9d11ce46595839"
|
||||
dependencies = [
|
||||
"ahash 0.8.3",
|
||||
"cfg-if",
|
||||
"crossbeam-channel",
|
||||
"crossterm",
|
||||
"cursive_core",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
"signal-hook",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cursive_core"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4db3b58161228d0dcb45c7968c5e74c3f03ad39e8983e58ad7d57061aa2cd94d"
|
||||
dependencies = [
|
||||
"ahash 0.8.3",
|
||||
"crossbeam-channel",
|
||||
"enum-map",
|
||||
"enumset",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"num",
|
||||
"owning_ref",
|
||||
"time 0.3.25",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
"xi-unicode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "daemon"
|
||||
version = "1.1.0-rc.14-dev"
|
||||
|
@ -1285,6 +1350,26 @@ dependencies = [
|
|||
"syn 2.0.29",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-map"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9705d8de4776df900a4a0b2384f8b0ab42f775e93b083b42f8ce71bdc32a47e3"
|
||||
dependencies = [
|
||||
"enum-map-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-map-derive"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccb14d927583dd5c2eac0f2cf264fc4762aefe1ae14c47a8a20fc1939d3a5fc0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.29",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2"
|
||||
version = "0.7.7"
|
||||
|
@ -1305,6 +1390,27 @@ dependencies = [
|
|||
"syn 2.0.29",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumset"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e875f1719c16de097dee81ed675e2d9bb63096823ed3f0ca827b7dea3028bbbb"
|
||||
dependencies = [
|
||||
"enumset_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumset_derive"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e08b6c6ab82d70f08844964ba10c7babb716de2ecaeab9be5717918a5177d3af"
|
||||
dependencies = [
|
||||
"darling 0.20.3",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.29",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
|
@ -2080,7 +2186,7 @@ dependencies = [
|
|||
"byteorder",
|
||||
"color_quant",
|
||||
"num-iter",
|
||||
"num-rational",
|
||||
"num-rational 0.3.2",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
|
@ -2322,13 +2428,16 @@ dependencies = [
|
|||
"clap",
|
||||
"clap_complete",
|
||||
"compact_jwt",
|
||||
"cursive",
|
||||
"dialoguer",
|
||||
"futures-concurrency",
|
||||
"kanidm_build_profiles",
|
||||
"kanidm_client",
|
||||
"kanidm_proto",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"qrcode",
|
||||
"regex",
|
||||
"rpassword 7.2.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
@ -2963,6 +3072,19 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af"
|
||||
dependencies = [
|
||||
"num-complex",
|
||||
"num-integer",
|
||||
"num-iter",
|
||||
"num-rational 0.4.1",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.3"
|
||||
|
@ -2974,6 +3096,15 @@ dependencies = [
|
|||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-complex"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.3.3"
|
||||
|
@ -3017,6 +3148,17 @@ dependencies = [
|
|||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.16"
|
||||
|
@ -3209,6 +3351,15 @@ version = "0.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "owning_ref"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ff55baddef9e4ad00f88b6c743a2a8062d4c6ade126c2a528644b8e444d52ce"
|
||||
dependencies = [
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pam_kanidm"
|
||||
version = "1.1.0-rc.14-dev"
|
||||
|
@ -4197,6 +4348,27 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-mio"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.1"
|
||||
|
@ -5446,6 +5618,12 @@ dependencies = [
|
|||
"time 0.3.25",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xi-unicode"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a67300977d3dc3f8034dae89778f502b6ba20b269527b3223ba59c0cf393bb8a"
|
||||
|
||||
[[package]]
|
||||
name = "yew"
|
||||
version = "0.20.0"
|
||||
|
|
65
book/src/developers/designs/diagrams/idv_api_diagram.drawio
Normal file
65
book/src/developers/designs/diagrams/idv_api_diagram.drawio
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 137 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 164 KiB |
145
book/src/developers/designs/diagrams/idv_state_machine.drawio
Normal file
145
book/src/developers/designs/diagrams/idv_state_machine.drawio
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 140 KiB |
39
book/src/developers/designs/identity_verification_feature.md
Normal file
39
book/src/developers/designs/identity_verification_feature.md
Normal file
|
@ -0,0 +1,39 @@
|
|||
# The identity verification API
|
||||
|
||||
The following diagram describes the api request/response of the identity verification feature (from here on referred as “idv”). The api takes an _IdentifyUserRequest_ instance as input, which in the diagram is represented by a circle shape, and it returns an _IdentifyUserResponse_, which is represented by a rectangle.
|
||||
The response rectangles are colored with green or red, and although all responses belong to the same enum, the colors are meant to provide additional information. A green response means that the input was valid and therefore it contains the next step in the identity verification flow, while a red response means the input was invalid and the flow terminates there. Note that the protocol is completely stateless, so the following diagram is not to be intended as a state machine, for the idv state machine go [here](#the-identity-verification-state-machine-idv).
|
||||
|
||||

|
||||
|
||||
Note that the endpoint path is _/v1/person/:id/\_identify_user_, therefore every request is made up by the _IdentifyUserRequest_ and an Id. Furthermore to use the api a user needs to be authenticated, so we link their userid to all their idv requests. Since all requests contains this additional information, there is a subset of responses that solely depend on it and therefore can **always** be returned regardless of what _IdentifyUserRequest_ what provided. Below you can find said subset along with an explanation for every response.
|
||||
|
||||

|
||||
|
||||
Here are the _IdentifyUserRequest_ and _IdentifyUserResponse_ enums just described as found inside the [source code](https://github.com/kanidm/kanidm/blob/05b35df413e017ca44cc4410cc255b63728ef373/proto/src/internal.rs#L32) :
|
||||
|
||||
```rust
|
||||
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,
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## The identity verification state machine
|
||||
|
||||
Here is the idv state machine, built on top of the idv endpoint request/response types previously described.
|
||||
Since the protocol provided by kanidm is completely stateless and doesn’t involve any online communication, some extra work is needed on the ui side to make things work. Specifically on the diagram you will notice some black arrows: they represent all the state transitions entirely driven by the ui without requiring any api call. You’ll also notice some empty rectangles with a red border: they represent the scenario in which the other user tells us that the code provided doesn’t match. This makes the idv fail, and it’s the only case in which the failure is entirely driven by the ui.
|
||||
|
||||

|
|
@ -225,7 +225,7 @@ 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);
|
||||
wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hceb9878d1477b426(arg0, arg1);
|
||||
}
|
||||
|
||||
let stack_pointer = 128;
|
||||
|
@ -237,7 +237,7 @@ function addBorrowedObject(obj) {
|
|||
}
|
||||
function __wbg_adapter_51(arg0, arg1, arg2) {
|
||||
try {
|
||||
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h575e9f970b01eb58(arg0, arg1, addBorrowedObject(arg2));
|
||||
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h3278f9f07368de56(arg0, arg1, addBorrowedObject(arg2));
|
||||
} finally {
|
||||
heap[stack_pointer++] = undefined;
|
||||
}
|
||||
|
@ -245,14 +245,14 @@ function __wbg_adapter_51(arg0, arg1, arg2) {
|
|||
|
||||
function __wbg_adapter_54(arg0, arg1, arg2) {
|
||||
try {
|
||||
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hb3e22b40c036b75b(arg0, arg1, addBorrowedObject(arg2));
|
||||
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hc8ade5491b0f90f8(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));
|
||||
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h4bea8770a6d5511b(arg0, arg1, addHeapObject(arg2));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1146,7 +1146,7 @@ function __wbg_get_imports() {
|
|||
const ret = wasm.memory;
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper1505 = function(arg0, arg1, arg2) {
|
||||
imports.wbg.__wbindgen_closure_wrapper1506 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 752, __wbg_adapter_48);
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
|
@ -1154,7 +1154,7 @@ function __wbg_get_imports() {
|
|||
const ret = makeMutClosure(arg0, arg1, 1975, __wbg_adapter_51);
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper4367 = function(arg0, arg1, arg2) {
|
||||
imports.wbg.__wbindgen_closure_wrapper4369 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 2069, __wbg_adapter_54);
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -60,10 +60,20 @@ 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")
|
||||
Regex::new(r"^\d{5,6}$").expect("Failed to parse VALIDATE_TOTP_RE") // TODO: add an error ID (internal error, restart)
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn totp_regex_test() {
|
||||
assert!(VALIDATE_TOTP_RE.is_match("123456"));
|
||||
assert!(VALIDATE_TOTP_RE.is_match("12345"));
|
||||
assert!(!VALIDATE_TOTP_RE.is_match("1234567"));
|
||||
assert!(!VALIDATE_TOTP_RE.is_match("1234"));
|
||||
assert!(!VALIDATE_TOTP_RE.is_match("12345a"));
|
||||
assert!(!VALIDATE_TOTP_RE.is_match("def not a totp"));
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IdentityVerificationApp {
|
||||
other_id: String,
|
||||
|
|
|
@ -14,6 +14,7 @@ repository = { workspace=true }
|
|||
|
||||
[features]
|
||||
default = ["unix"]
|
||||
idv-tui = ["dep:cursive"]
|
||||
unix = []
|
||||
|
||||
[lib]
|
||||
|
@ -50,6 +51,14 @@ tokio = { workspace = true, features = ["rt", "macros"] }
|
|||
url = { workspace = true, features = ["serde"] }
|
||||
uuid = { workspace=true }
|
||||
zxcvbn = { workspace=true }
|
||||
lazy_static.workspace = true
|
||||
regex.workspace = true
|
||||
|
||||
[dependencies.cursive]
|
||||
version = "0.20.0"
|
||||
optional = true
|
||||
default-features = false
|
||||
features = ["crossterm-backend"]
|
||||
|
||||
[build-dependencies]
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
|
|
603
tools/cli/src/cli/identify_user_tui.rs
Normal file
603
tools/cli/src/cli/identify_user_tui.rs
Normal file
|
@ -0,0 +1,603 @@
|
|||
use cursive::{
|
||||
align::HAlign,
|
||||
crossterm,
|
||||
view::{Nameable, Resizable},
|
||||
views::{Dialog, DummyView, EditView, LinearLayout, TextArea, TextView},
|
||||
CbSink, Cursive, CursiveRunnable, View,
|
||||
};
|
||||
use kanidm_client::KanidmClient;
|
||||
use kanidm_proto::internal::{IdentifyUserRequest, IdentifyUserResponse};
|
||||
use std::{cell::RefCell, sync::Arc, time::SystemTime};
|
||||
use tokio::sync::{
|
||||
mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
|
||||
oneshot::{self, Receiver},
|
||||
};
|
||||
|
||||
// here I used a simple function instead of a struct because all the channel stuff requires ownership, so if we were to use a struct with a `run` method, it would have to take ownership of everything
|
||||
// so might as well just use a function
|
||||
pub async fn run_identity_verification_tui(self_id: &str, client: KanidmClient) {
|
||||
//unbounded channel to send messages to the controller from the ui
|
||||
let (controller_tx, controller_rx) = unbounded_channel::<IdentifyUserMsg>();
|
||||
// unbounded channel to send messages to the ui from the controller
|
||||
let (ui_tx, ui_rx) = unbounded_channel::<IdentifyUserState>();
|
||||
|
||||
// we manually send the initial start message
|
||||
if controller_tx.send(IdentifyUserMsg::Start).is_err() {
|
||||
eprint!("Failed to send the initial start message to the controller! Aborting..."); // TODO: add an error ID (internal error, restart)
|
||||
return;
|
||||
};
|
||||
|
||||
// oneshot channel to get the callback sink from the ui
|
||||
let (cb_tx, cb_rx) = oneshot::channel::<CbSink>();
|
||||
// we start the ui in its own thread
|
||||
let gui_handle = std::thread::spawn(move || {
|
||||
let mut ui = Ui::new(controller_tx, ui_rx);
|
||||
if cb_tx.send(ui.get_cb()).is_err() {
|
||||
eprintln!("Internal callback error in the CLI's TUI, please restart or log an issue with the Kanidm project if it continues to occur."); // TODO: add an error ID (internal error, restart)
|
||||
return;
|
||||
};
|
||||
ui.0.run();
|
||||
});
|
||||
|
||||
start_business_logic_loop(controller_rx, cb_rx, ui_tx, self_id, client).await;
|
||||
|
||||
if let Err(e) = gui_handle.join() {
|
||||
eprintln!(
|
||||
"The UI thread returned an error, please restart the program. Error was: {:?}",
|
||||
e
|
||||
); // TODO: add an error ID (internal error, restart)
|
||||
};
|
||||
}
|
||||
|
||||
async fn start_business_logic_loop(
|
||||
mut controller_rx: UnboundedReceiver<IdentifyUserMsg>,
|
||||
cb_rx: Receiver<CbSink>,
|
||||
ui_tx: UnboundedSender<IdentifyUserState>,
|
||||
self_id: &str,
|
||||
client: KanidmClient,
|
||||
) {
|
||||
let Ok(cb) = cb_rx.await else {
|
||||
eprintln!("Internal callback error in the CLI's logic loop, please restart or log an issue with the Kanidm project if it continues to occur."); // TODO: add an error ID (internal error, restart)
|
||||
return;
|
||||
};
|
||||
|
||||
let send_msg_and_call_callback = |msg: IdentifyUserState| {
|
||||
if ui_tx.send(msg).is_err() {
|
||||
eprintln!("The UI thread returned an error, please restart the program.");
|
||||
// TODO: add an error ID (internal error, restart)
|
||||
}
|
||||
if cb.send(Box::new(Ui::update_state_callback)).is_err() {
|
||||
eprintln!("The UI thread returned an error, please restart the program.");
|
||||
// TODO: add an error ID (internal error, restart)
|
||||
};
|
||||
};
|
||||
let self_id = Arc::new(self_id.to_string());
|
||||
// conveniently when the `quit()` is called on the ui it also drops the controller_tx since it's stored in the `user_data` so as per the doc `controller_rx.recv()` will return None and therefore the loop will exit
|
||||
while let Some(msg) = controller_rx.recv().await {
|
||||
// ** NEVER EVER CALL `break` inside the loop as it will drop the mpsc receiver and sender and the ui won't be able to process whatever message is sent to it
|
||||
// ** instead use `continue` so that the loop will only exit when the ui drops its controllers
|
||||
let (id, req) = match &msg {
|
||||
IdentifyUserMsg::Start => (&self_id, IdentifyUserRequest::Start),
|
||||
IdentifyUserMsg::SubmitOtherId { other_id } => (other_id, IdentifyUserRequest::Start),
|
||||
IdentifyUserMsg::SubmitCode {
|
||||
code: totp,
|
||||
other_id,
|
||||
} => (
|
||||
other_id,
|
||||
IdentifyUserRequest::SubmitCode { other_totp: *totp },
|
||||
),
|
||||
IdentifyUserMsg::CodeConfirmedFirst { other_id } => {
|
||||
send_msg_and_call_callback(IdentifyUserState::WaitForCode {
|
||||
other_id: other_id.clone(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
IdentifyUserMsg::CodeConfirmedSecond { other_id } => {
|
||||
send_msg_and_call_callback(IdentifyUserState::Success {
|
||||
other_id: other_id.clone(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
IdentifyUserMsg::ReDisplayCodeFirst { other_id }
|
||||
| IdentifyUserMsg::ReDisplayCodeSecond { other_id } => {
|
||||
(other_id, IdentifyUserRequest::DisplayCode)
|
||||
}
|
||||
};
|
||||
let res = match client.idm_person_identify_user(id, req).await {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
let err = IdentifyUserState::Error {
|
||||
error_title: "Server error!".to_string(),
|
||||
error_msg: format!("{:?}", e),
|
||||
};
|
||||
send_msg_and_call_callback(err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let state = match res {
|
||||
IdentifyUserResponse::IdentityVerificationUnavailable => IdentifyUserState::Error {
|
||||
error_title: "Feature unavailable".to_string(),
|
||||
error_msg: IDENTITY_UNAVAILABLE_ERROR_MESSAGE.to_string(),
|
||||
},
|
||||
IdentifyUserResponse::IdentityVerificationAvailable => {
|
||||
IdentifyUserState::IdDisplayAndSubmit {
|
||||
self_id: self_id.clone(),
|
||||
}
|
||||
}
|
||||
IdentifyUserResponse::ProvideCode { step, totp } => match msg {
|
||||
IdentifyUserMsg::SubmitOtherId { other_id }
|
||||
| IdentifyUserMsg::ReDisplayCodeFirst { other_id } => {
|
||||
IdentifyUserState::DisplayCodeFirst {
|
||||
self_totp: totp,
|
||||
step,
|
||||
other_id,
|
||||
}
|
||||
}
|
||||
IdentifyUserMsg::SubmitCode { other_id, .. }
|
||||
| IdentifyUserMsg::ReDisplayCodeSecond { other_id } => {
|
||||
IdentifyUserState::DisplayCodeSecond {
|
||||
self_totp: totp,
|
||||
step,
|
||||
other_id,
|
||||
}
|
||||
}
|
||||
_ => IdentifyUserState::invalid_state_error(),
|
||||
},
|
||||
IdentifyUserResponse::WaitForCode => match msg {
|
||||
IdentifyUserMsg::SubmitOtherId { other_id }
|
||||
| IdentifyUserMsg::SubmitCode { other_id, .. } => {
|
||||
IdentifyUserState::WaitForCode { other_id }
|
||||
}
|
||||
_ => IdentifyUserState::invalid_state_error(),
|
||||
},
|
||||
IdentifyUserResponse::Success => match msg {
|
||||
IdentifyUserMsg::SubmitCode { other_id, .. } => {
|
||||
IdentifyUserState::Success { other_id }
|
||||
}
|
||||
_ => IdentifyUserState::invalid_state_error(),
|
||||
},
|
||||
IdentifyUserResponse::CodeFailure => match msg {
|
||||
IdentifyUserMsg::SubmitCode { .. } => IdentifyUserState::Error {
|
||||
error_title: "🚨 Identity verification failed 🚨".to_string(),
|
||||
error_msg: CODE_FAILURE_ERROR_MESSAGE.to_string(),
|
||||
},
|
||||
_ => IdentifyUserState::invalid_state_error(),
|
||||
},
|
||||
IdentifyUserResponse::InvalidUserId => IdentifyUserState::Error {
|
||||
error_msg: format!("{id} {INVALID_USER_ID_ERROR_MESSAGE}"),
|
||||
error_title: "Invalid ID error".to_string(),
|
||||
},
|
||||
};
|
||||
send_msg_and_call_callback(state);
|
||||
}
|
||||
}
|
||||
|
||||
// this is kind of awkward but Cursive doesn't allow us to store data in the `user_data` without having to clone it every time we access it,
|
||||
// so since all the Strings will never change during the execution of the program, we can just use Arcs to avoid cloning them every time
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum IdentifyUserState {
|
||||
IdDisplayAndSubmit {
|
||||
self_id: Arc<String>,
|
||||
},
|
||||
WaitForCode {
|
||||
other_id: Arc<String>,
|
||||
},
|
||||
DisplayCodeFirst {
|
||||
self_totp: u32,
|
||||
step: u32,
|
||||
other_id: Arc<String>,
|
||||
},
|
||||
DisplayCodeSecond {
|
||||
self_totp: u32,
|
||||
step: u32,
|
||||
other_id: Arc<String>,
|
||||
},
|
||||
Success {
|
||||
other_id: Arc<String>,
|
||||
},
|
||||
Error {
|
||||
error_msg: String,
|
||||
error_title: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl IdentifyUserState {
|
||||
pub fn invalid_state_error() -> Self {
|
||||
IdentifyUserState::Error {
|
||||
error_msg: INVALID_STATE_ERROR_MESSAGE.to_string(), // TODO: add an error ID (internal error, restart)
|
||||
error_title: "Invalid flow detected!".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum IdentifyUserMsg {
|
||||
Start,
|
||||
SubmitOtherId { other_id: Arc<String> },
|
||||
SubmitCode { code: u32, other_id: Arc<String> },
|
||||
CodeConfirmedFirst { other_id: Arc<String> },
|
||||
CodeConfirmedSecond { other_id: Arc<String> },
|
||||
ReDisplayCodeFirst { other_id: Arc<String> },
|
||||
ReDisplayCodeSecond { other_id: Arc<String> },
|
||||
}
|
||||
|
||||
struct Ui(CursiveRunnable);
|
||||
|
||||
struct UiUserData {
|
||||
controller_tx: UnboundedSender<IdentifyUserMsg>,
|
||||
ui_rx: UnboundedReceiver<IdentifyUserState>,
|
||||
}
|
||||
|
||||
impl Ui {
|
||||
fn new(
|
||||
controller_tx: UnboundedSender<IdentifyUserMsg>,
|
||||
ui_rx: UnboundedReceiver<IdentifyUserState>,
|
||||
) -> Self {
|
||||
let mut cursive = crossterm();
|
||||
cursive.add_global_callback('q', |s| {
|
||||
s.quit();
|
||||
});
|
||||
cursive.set_autorefresh(true);
|
||||
cursive.set_user_data(UiUserData {
|
||||
controller_tx,
|
||||
ui_rx,
|
||||
});
|
||||
|
||||
Ui(cursive)
|
||||
}
|
||||
|
||||
fn get_cb(&self) -> CbSink {
|
||||
self.0.cb_sink().clone()
|
||||
}
|
||||
|
||||
fn render_state(
|
||||
s: &mut Cursive,
|
||||
state: IdentifyUserState,
|
||||
controller_tx: UnboundedSender<IdentifyUserMsg>,
|
||||
) {
|
||||
match state {
|
||||
IdentifyUserState::IdDisplayAndSubmit { self_id } => {
|
||||
let controller_tx_clone = controller_tx.clone();
|
||||
let layout = LinearLayout::vertical()
|
||||
.child(DummyView.fixed_height(1))
|
||||
.child(
|
||||
TextView::new(format!(
|
||||
"When asked for your ID, provide the following: {}",
|
||||
self_id
|
||||
))
|
||||
.h_align(HAlign::Center),
|
||||
)
|
||||
.child(DummyView.fixed_height(1))
|
||||
.child(
|
||||
TextView::new(" ---------------------------------------------- ")
|
||||
.h_align(HAlign::Center),
|
||||
)
|
||||
.child(DummyView.fixed_height(1))
|
||||
.child(
|
||||
TextView::new("Ask for the other person's ID, and insert it here!")
|
||||
.h_align(HAlign::Center),
|
||||
)
|
||||
.child(DummyView.fixed_height(1))
|
||||
.child(
|
||||
EditView::new()
|
||||
.on_submit(move |s, user_id: &str| {
|
||||
let send_outcome =
|
||||
controller_tx.send(IdentifyUserMsg::SubmitOtherId {
|
||||
other_id: Arc::new(user_id.to_string()),
|
||||
});
|
||||
if send_outcome.is_err() {
|
||||
s.quit();
|
||||
};
|
||||
Self::loading_view(s);
|
||||
})
|
||||
.with_name("id-user-input"),
|
||||
);
|
||||
// we have to redeclare this because we consumed it in the prev closure
|
||||
s.add_layer(
|
||||
Dialog::around(layout)
|
||||
.button("Quit", |s| {
|
||||
s.quit();
|
||||
})
|
||||
.button("Continue", move |s| {
|
||||
let user_id = match s
|
||||
.call_on_name("id-user-input", |view: &mut EditView| {
|
||||
view.get_content()
|
||||
}) {
|
||||
Some(user_id) => user_id,
|
||||
None => {
|
||||
return Self::error_state_view(
|
||||
s,
|
||||
"Internal error, couldn't get the 'id-user-input' view, please restart the program.", // TODO: add an error ID (internal error, restart)
|
||||
None,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let send_outcome =
|
||||
controller_tx_clone.send(IdentifyUserMsg::SubmitOtherId {
|
||||
other_id: Arc::new(user_id.to_string()),
|
||||
});
|
||||
if send_outcome.is_err() {
|
||||
s.quit();
|
||||
};
|
||||
Self::loading_view(s);
|
||||
}),
|
||||
);
|
||||
}
|
||||
IdentifyUserState::WaitForCode { other_id } => {
|
||||
s.pop_layer();
|
||||
let other_id_clone = other_id.clone();
|
||||
let controller_tx_clone = controller_tx.clone();
|
||||
let layout = LinearLayout::vertical()
|
||||
.child(TextView::new(format!(
|
||||
"Ask for {}'s code, and insert it here!",
|
||||
&other_id
|
||||
)))
|
||||
.child(DummyView.fixed_height(1))
|
||||
.child(
|
||||
EditView::new()
|
||||
.on_submit(move |s, code: &str| {
|
||||
let code_u32 =
|
||||
match Self::parse_totp_code_and_display_popup(s, code) {
|
||||
Some(code) => code,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let send_outcome =
|
||||
controller_tx.send(IdentifyUserMsg::SubmitCode {
|
||||
code: code_u32,
|
||||
other_id: other_id_clone.clone(),
|
||||
});
|
||||
if send_outcome.is_err() {
|
||||
s.quit();
|
||||
};
|
||||
Self::loading_view(s);
|
||||
})
|
||||
.with_name("totp-input"),
|
||||
);
|
||||
s.add_layer(
|
||||
Dialog::around(layout)
|
||||
.button("Quit", |s| {
|
||||
s.quit();
|
||||
})
|
||||
.button("Continue", move |s| {
|
||||
let code = match s.call_on_name("totp-input", |view: &mut EditView| {
|
||||
view.get_content()
|
||||
}) {
|
||||
Some(code) => code,
|
||||
None => {
|
||||
return Self::error_state_view(
|
||||
s,
|
||||
"Internal error, couldn't get the 'totp-input' view, please restart the program.", // TODO: add an error ID (internal error, restart)
|
||||
None,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let code_u32 =
|
||||
match Self::parse_totp_code_and_display_popup(s, code.as_str()) {
|
||||
Some(code) => code,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let send_outcome =
|
||||
controller_tx_clone.send(IdentifyUserMsg::SubmitCode {
|
||||
code: code_u32,
|
||||
other_id: other_id.clone(),
|
||||
});
|
||||
if send_outcome.is_err() {
|
||||
s.quit();
|
||||
};
|
||||
Self::loading_view(s);
|
||||
}),
|
||||
);
|
||||
}
|
||||
IdentifyUserState::DisplayCodeFirst {
|
||||
self_totp,
|
||||
step,
|
||||
other_id,
|
||||
} => {
|
||||
s.pop_layer();
|
||||
let layout = LinearLayout::vertical()
|
||||
.child(TextView::new(format!(
|
||||
"Provide the following code when asked: {self_totp}"
|
||||
)))
|
||||
.child(DummyView.fixed_height(1))
|
||||
.child(TotpCountdownView::new(
|
||||
step as u64,
|
||||
controller_tx.clone(),
|
||||
IdentifyUserMsg::ReDisplayCodeFirst {
|
||||
other_id: other_id.clone(),
|
||||
},
|
||||
));
|
||||
s.add_layer(Dialog::around(layout).button("Continue", move |s| {
|
||||
Self::confirmation_view(
|
||||
s,
|
||||
&other_id,
|
||||
controller_tx.clone(),
|
||||
IdentifyUserMsg::CodeConfirmedFirst {
|
||||
other_id: other_id.clone(),
|
||||
},
|
||||
);
|
||||
}));
|
||||
}
|
||||
IdentifyUserState::DisplayCodeSecond {
|
||||
self_totp,
|
||||
step,
|
||||
other_id,
|
||||
} => {
|
||||
s.pop_layer();
|
||||
let layout = LinearLayout::vertical()
|
||||
.child(TextView::new(format!(
|
||||
"Provide the following code when asked: {self_totp}"
|
||||
)))
|
||||
.child(DummyView.fixed_height(1))
|
||||
.child(TotpCountdownView::new(
|
||||
step as u64,
|
||||
controller_tx.clone(),
|
||||
IdentifyUserMsg::ReDisplayCodeSecond {
|
||||
other_id: other_id.clone(),
|
||||
},
|
||||
));
|
||||
s.add_layer(Dialog::around(layout).button("Continue", move |s| {
|
||||
Self::confirmation_view(
|
||||
s,
|
||||
&other_id,
|
||||
controller_tx.clone(),
|
||||
IdentifyUserMsg::CodeConfirmedSecond {
|
||||
other_id: other_id.clone(),
|
||||
},
|
||||
);
|
||||
}));
|
||||
}
|
||||
IdentifyUserState::Success { other_id } => {
|
||||
s.pop_layer();
|
||||
let layout = LinearLayout::vertical().child(TextView::new(format!(
|
||||
"{other_id}'s identity has been successfully verified!"
|
||||
)));
|
||||
|
||||
s.add_layer(
|
||||
Dialog::around(layout)
|
||||
.padding_lrtb(1, 1, 1, 0)
|
||||
.title("Success 🎉🎉")
|
||||
.button("Quit", |s| {
|
||||
s.quit();
|
||||
}),
|
||||
);
|
||||
}
|
||||
IdentifyUserState::Error {
|
||||
error_msg: msg,
|
||||
error_title: title,
|
||||
} => Self::error_state_view(s, &msg, Some(&title)),
|
||||
};
|
||||
}
|
||||
|
||||
fn update_state_callback(s: &mut Cursive) {
|
||||
let user_data = match s.user_data::<UiUserData>() {
|
||||
Some(data) => data,
|
||||
None => {
|
||||
return Self::error_state_view(
|
||||
s,
|
||||
"Failed to parse server response, please start again.", // TODO: add error ID (internal error, restart)
|
||||
None,
|
||||
);
|
||||
}
|
||||
};
|
||||
if let Some(state) = user_data.ui_rx.blocking_recv() {
|
||||
let controller_rx = user_data.controller_tx.to_owned(); // we have to take ownership so the mut borrow `s` can be passed to `render_state`
|
||||
Ui::render_state(s, state, controller_rx);
|
||||
}
|
||||
}
|
||||
|
||||
fn confirmation_view(
|
||||
s: &mut Cursive,
|
||||
other_id: &Arc<String>,
|
||||
controller_tx: UnboundedSender<IdentifyUserMsg>,
|
||||
msg: IdentifyUserMsg,
|
||||
) {
|
||||
let textarea = TextArea::new().content(format!("Did you confirm that {other_id} correctly verified your code? If you proceed, you won't be able to go back.")).disabled().fixed_width(57);
|
||||
s.add_layer(
|
||||
Dialog::around(textarea)
|
||||
.padding_lrtb(1, 1, 0, 1)
|
||||
.title("Warning!")
|
||||
.button("Continue", move |s| {
|
||||
s.pop_layer();
|
||||
s.pop_layer();
|
||||
let send_outcome = controller_tx.send(msg.to_owned());
|
||||
if send_outcome.is_err() {
|
||||
s.quit();
|
||||
};
|
||||
})
|
||||
.dismiss_button("Cancel"),
|
||||
);
|
||||
}
|
||||
|
||||
fn error_state_view(s: &mut Cursive, msg: &str, error_title: Option<&str>) {
|
||||
s.pop_layer();
|
||||
let layout = LinearLayout::vertical()
|
||||
.child(DummyView.fixed_height(1))
|
||||
.child(TextView::new(msg));
|
||||
|
||||
s.add_layer(
|
||||
Dialog::around(layout)
|
||||
.title(error_title.unwrap_or("An error occurred!"))
|
||||
.button("Quit", |s| {
|
||||
s.quit();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
fn parse_totp_code_and_display_popup(s: &mut Cursive, code: &str) -> Option<u32> {
|
||||
let code_u32 = match code.parse::<u32>() {
|
||||
Ok(code_u32) => code_u32,
|
||||
Err(_) => {
|
||||
Self::disposable_warning_view(s, "The code you provided is not a number!");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if code.len() < 5 || code.len() > 6 {
|
||||
Self::disposable_warning_view(s, "The code should be a 5 or 6 digit number!");
|
||||
return None;
|
||||
};
|
||||
Some(code_u32)
|
||||
}
|
||||
|
||||
fn disposable_warning_view(s: &mut Cursive, msg: &str) {
|
||||
let dialog = Dialog::text(msg).dismiss_button("Ok");
|
||||
s.add_layer(dialog);
|
||||
}
|
||||
|
||||
fn loading_view(s: &mut Cursive) {
|
||||
s.pop_layer();
|
||||
s.add_layer(TextView::new("Loading, please wait..."));
|
||||
}
|
||||
}
|
||||
|
||||
struct TotpCountdownView {
|
||||
msg: IdentifyUserMsg,
|
||||
step: u64,
|
||||
controller_tx: UnboundedSender<IdentifyUserMsg>,
|
||||
should_call_callback: RefCell<bool>, // we need to use a refcell since we need to mutate this data inside the `draw` method which has a `&self` reference
|
||||
}
|
||||
|
||||
impl TotpCountdownView {
|
||||
fn new(
|
||||
step: u64,
|
||||
controller_tx: UnboundedSender<IdentifyUserMsg>,
|
||||
msg: IdentifyUserMsg,
|
||||
) -> Self {
|
||||
Self {
|
||||
should_call_callback: RefCell::new(true),
|
||||
msg,
|
||||
step,
|
||||
controller_tx,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_ticks_left_from_now(&self, step: u64) -> u64 {
|
||||
#[allow(clippy::expect_used)]
|
||||
let dur = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.expect("invalid duration from epoch now");
|
||||
step - dur.as_secs() % (step)
|
||||
}
|
||||
}
|
||||
|
||||
impl View for TotpCountdownView {
|
||||
fn draw(&self, printer: &cursive::Printer) {
|
||||
let ticks_left_from_now = self.get_ticks_left_from_now(self.step);
|
||||
// basically whenever the ticks_left reset to step, i.e. the first time this function has been called after we got to a new totp window, then we
|
||||
// call the callback to fetch a new code which will be displayed at best in the next tick. On very slow connections the user might see the old
|
||||
// code for a bit. If we want to get rid of this we would need to pass to the struct a callback to show a loading screen
|
||||
if ticks_left_from_now == self.step && *self.should_call_callback.borrow() {
|
||||
self.controller_tx
|
||||
.send(self.msg.to_owned())
|
||||
.expect("TOTP countdown view failed to send msg to controller"); // TODO: add an error ID (internal error, restart)
|
||||
*self.should_call_callback.borrow_mut() = false;
|
||||
};
|
||||
printer.print(
|
||||
(0, 0),
|
||||
&format!(" {}s left", ticks_left_from_now),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -10,12 +10,17 @@
|
|||
#![deny(clippy::trivially_copy_pass_by_ref)]
|
||||
// We allow expect since it forces good error messages at the least.
|
||||
#![allow(clippy::expect_used)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate tracing;
|
||||
|
||||
use crate::common::OpType;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(not(feature = "idv-tui"))]
|
||||
use identify_user_no_tui::{run_identity_verification_no_tui, IdentifyUserState};
|
||||
#[cfg(feature = "idv-tui")]
|
||||
use identify_user_tui::run_identity_verification_tui;
|
||||
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
@ -25,6 +30,8 @@ pub mod badlist;
|
|||
pub mod common;
|
||||
pub mod domain;
|
||||
pub mod group;
|
||||
#[cfg(feature = "idv-tui")]
|
||||
mod identify_user_tui;
|
||||
pub mod oauth2;
|
||||
pub mod person;
|
||||
pub mod raw;
|
||||
|
@ -39,6 +46,7 @@ impl SelfOpt {
|
|||
pub fn debug(&self) -> bool {
|
||||
match self {
|
||||
SelfOpt::Whoami(copt) => copt.debug,
|
||||
SelfOpt::IdentifyUser(copt) => copt.debug,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,6 +70,38 @@ impl SelfOpt {
|
|||
Err(e) => println!("Error: {:?}", e),
|
||||
}
|
||||
}
|
||||
SelfOpt::IdentifyUser(copt) => {
|
||||
let client = copt.to_client(OpType::Write).await;
|
||||
let whoami_response = match client.whoami().await {
|
||||
Ok(o_ent) => {
|
||||
match o_ent {
|
||||
Some(ent) => ent,
|
||||
None => {
|
||||
eprintln!("Authentication with cached token failed, can't query information."); // TODO: add an error ID (login, or clear token cache)
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error querying whoami endpoint: {:?}", e); // TODO: add an error ID (internal/web response error, restart or check connectivity)
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let spn =
|
||||
match whoami_response.attrs.get("spn").and_then(|v| v.first()) {
|
||||
Some(spn) => spn,
|
||||
None => {
|
||||
eprintln!("Failed to parse your SPN from the system's whoami endpoint, exiting!"); // TODO: add an error ID (internal/web response error, restart)
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(feature = "idv-tui")]
|
||||
run_identity_verification_tui(spn, client).await;
|
||||
#[cfg(not(feature = "idv-tui"))]
|
||||
run_identity_verification_no_tui(IdentifyUserState::Start, client, spn, None).await;
|
||||
} // end PersonOpt::Validity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -144,3 +184,254 @@ pub(crate) fn password_prompt(prompt: &str) -> Option<String> {
|
|||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub const IDENTITY_UNAVAILABLE_ERROR_MESSAGE: &str = "The identity verification feature is not enabled for your account, please contact an administrator.";
|
||||
pub const CODE_FAILURE_ERROR_MESSAGE: &str = "The provided code doesn't match, please try again.";
|
||||
pub const INVALID_USER_ID_ERROR_MESSAGE: &str =
|
||||
"account exists but cannot access the identity verification feature 😕";
|
||||
pub const INVALID_STATE_ERROR_MESSAGE: &str =
|
||||
"The user identification flow is in an invalid state 😵😵";
|
||||
|
||||
#[cfg(not(feature = "idv-tui"))]
|
||||
mod identify_user_no_tui {
|
||||
use crate::{
|
||||
CODE_FAILURE_ERROR_MESSAGE, IDENTITY_UNAVAILABLE_ERROR_MESSAGE,
|
||||
INVALID_STATE_ERROR_MESSAGE, INVALID_USER_ID_ERROR_MESSAGE,
|
||||
};
|
||||
|
||||
use kanidm_client::{ClientError, KanidmClient};
|
||||
use kanidm_proto::internal::{IdentifyUserRequest, IdentifyUserResponse};
|
||||
|
||||
use dialoguer::{Confirm, Input};
|
||||
use regex::Regex;
|
||||
use std::{
|
||||
io::{stdout, Write},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref VALIDATE_TOTP_RE: Regex = {
|
||||
#[allow(clippy::expect_used)]
|
||||
Regex::new(r"^\d{5,6}$").expect("Failed to parse VALIDATE_TOTP_RE") // TODO: add an error ID (internal error, restart)
|
||||
};
|
||||
}
|
||||
|
||||
pub(super) enum IdentifyUserState {
|
||||
Start,
|
||||
IdDisplayAndSubmit,
|
||||
SubmitCode,
|
||||
DisplayCodeFirst { self_totp: u32, step: u32 },
|
||||
DisplayCodeSecond { self_totp: u32, step: u32 },
|
||||
}
|
||||
|
||||
fn server_error(e: &ClientError) {
|
||||
eprintln!("Server error!"); // TODO: add an error ID (internal error, restart)
|
||||
eprintln!("{:?}", e);
|
||||
println!("Exiting...");
|
||||
}
|
||||
|
||||
pub(super) async fn run_identity_verification_no_tui<'a>(
|
||||
mut state: IdentifyUserState,
|
||||
client: KanidmClient,
|
||||
self_id: &'a str,
|
||||
mut other_id: Option<String>,
|
||||
) {
|
||||
loop {
|
||||
match state {
|
||||
IdentifyUserState::Start => {
|
||||
let res = match &client
|
||||
.idm_person_identify_user(self_id, IdentifyUserRequest::Start)
|
||||
.await
|
||||
{
|
||||
Ok(res) => res.clone(),
|
||||
Err(e) => {
|
||||
return server_error(e);
|
||||
}
|
||||
};
|
||||
match res {
|
||||
IdentifyUserResponse::IdentityVerificationUnavailable => {
|
||||
println!("{IDENTITY_UNAVAILABLE_ERROR_MESSAGE}");
|
||||
return;
|
||||
}
|
||||
IdentifyUserResponse::IdentityVerificationAvailable => {
|
||||
state = IdentifyUserState::IdDisplayAndSubmit;
|
||||
}
|
||||
_ => {
|
||||
eprintln!("{INVALID_STATE_ERROR_MESSAGE}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
IdentifyUserState::IdDisplayAndSubmit => {
|
||||
println!("When asked for your ID, provide the following: {self_id}");
|
||||
|
||||
// Display Prompt
|
||||
let other_user_id: String = Input::new()
|
||||
.with_prompt("Ask for the other person's ID, and insert it here")
|
||||
.interact_text()
|
||||
.expect("Failed to interact with interactive session");
|
||||
let _ = stdout().flush();
|
||||
|
||||
let res = match &client
|
||||
.idm_person_identify_user(&other_user_id, IdentifyUserRequest::Start)
|
||||
.await
|
||||
{
|
||||
Ok(res) => res.clone(),
|
||||
Err(e) => {
|
||||
return server_error(e);
|
||||
}
|
||||
};
|
||||
match res {
|
||||
IdentifyUserResponse::WaitForCode => {
|
||||
state = IdentifyUserState::SubmitCode;
|
||||
|
||||
other_id = Some(other_user_id);
|
||||
}
|
||||
IdentifyUserResponse::ProvideCode { step, totp } => {
|
||||
state = IdentifyUserState::DisplayCodeFirst {
|
||||
self_totp: totp,
|
||||
step,
|
||||
};
|
||||
|
||||
other_id = Some(other_user_id);
|
||||
}
|
||||
IdentifyUserResponse::InvalidUserId => {
|
||||
eprintln!("{other_user_id} {INVALID_USER_ID_ERROR_MESSAGE}");
|
||||
return;
|
||||
}
|
||||
_ => {
|
||||
eprintln!("{INVALID_STATE_ERROR_MESSAGE}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
IdentifyUserState::SubmitCode => {
|
||||
// Display Prompt
|
||||
let other_totp: String = Input::new()
|
||||
.with_prompt("Insert here the other person code")
|
||||
.validate_with(|s: &String| -> Result<(), &str> {
|
||||
if VALIDATE_TOTP_RE.is_match(s) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("The code should be a 5 or 6 digit number")
|
||||
}
|
||||
})
|
||||
.interact_text()
|
||||
.expect("Failed to interact with interactive session");
|
||||
|
||||
let res = match &client
|
||||
.idm_person_identify_user(
|
||||
other_id.as_deref().unwrap_or_default(),
|
||||
IdentifyUserRequest::SubmitCode {
|
||||
other_totp: other_totp.parse().unwrap_or_default(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(res) => res.clone(),
|
||||
Err(e) => {
|
||||
return server_error(e);
|
||||
}
|
||||
};
|
||||
match res {
|
||||
IdentifyUserResponse::CodeFailure => {
|
||||
eprintln!("{CODE_FAILURE_ERROR_MESSAGE}");
|
||||
return;
|
||||
}
|
||||
IdentifyUserResponse::Success => {
|
||||
println!(
|
||||
"{}'s identity has been successfully verified 🎉🎉",
|
||||
other_id.as_deref().unwrap_or_default()
|
||||
);
|
||||
return;
|
||||
}
|
||||
IdentifyUserResponse::InvalidUserId => {
|
||||
eprintln!(
|
||||
"{} {INVALID_USER_ID_ERROR_MESSAGE}",
|
||||
other_id.as_deref().unwrap_or_default()
|
||||
);
|
||||
return;
|
||||
}
|
||||
IdentifyUserResponse::ProvideCode { step, totp } => {
|
||||
// since we have already inserted the code, we have to go to display code second,
|
||||
state = IdentifyUserState::DisplayCodeSecond {
|
||||
self_totp: totp,
|
||||
step,
|
||||
};
|
||||
}
|
||||
|
||||
_ => {
|
||||
eprintln!("{INVALID_STATE_ERROR_MESSAGE}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
IdentifyUserState::DisplayCodeFirst { self_totp, step } => {
|
||||
println!("Provide the following code when asked: {}", self_totp);
|
||||
let seconds_left = get_ms_left_from_now(step as u128) / 1000;
|
||||
println!("This codes expires in {seconds_left} seconds");
|
||||
let _ = stdout().flush();
|
||||
if !matches!(Confirm::new().with_prompt("Continue?").interact(), Ok(true)) {
|
||||
println!("Identity verification failed. Exiting...");
|
||||
return;
|
||||
}
|
||||
match Confirm::new()
|
||||
.with_prompt(format!("Did you confirm that {} correctly verified your code? If you proceed, you won't be able to go back.", other_id.as_deref().unwrap_or_default()))
|
||||
.interact() {
|
||||
Ok(true) => {println!("Code confirmed, continuing...")}
|
||||
Ok(false) => {
|
||||
println!("Identity verification failed. Exiting...");
|
||||
return;
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("An error occurred while trying to read from stderr: {:?}", e); // TODO: add error ID (internal error, restart)
|
||||
println!("Exiting...");
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
state = IdentifyUserState::SubmitCode;
|
||||
}
|
||||
IdentifyUserState::DisplayCodeSecond { self_totp, step } => {
|
||||
println!("Provide the following code when asked: {}", self_totp);
|
||||
let seconds_left = get_ms_left_from_now(step as u128) / 1000;
|
||||
println!("This codes expires in {seconds_left} seconds!");
|
||||
let _ = stdout().flush();
|
||||
if !matches!(Confirm::new().with_prompt("Continue?").interact(), Ok(true)) {
|
||||
println!("Identity verification failed. Exiting...");
|
||||
return;
|
||||
}
|
||||
match Confirm::new()
|
||||
.with_prompt(format!("Did you confirm that {} correctly verified your code? If you proceed, you won't be able to go back.", other_id.as_deref().unwrap_or_default()))
|
||||
.interact() {
|
||||
Ok(true) => {println!(
|
||||
"{}'s identity has been successfully verified 🎉🎉",
|
||||
other_id.take().unwrap_or_default()
|
||||
);
|
||||
return;}
|
||||
Ok(false) => {
|
||||
println!("Exiting...");
|
||||
return;
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("An error occurred while trying to read from stderr: {:?}", e); // TODO: add error ID (internal error, restart)
|
||||
println!("Exiting...");
|
||||
return;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: this function is somewhat a duplicate of what can be found in the webui, see https://github.com/kanidm/kanidm/blob/003234c2d0a52146683628156e2a106bf61fe9f4/server/web_ui/src/components/totpdisplay.rs#L83
|
||||
// * should we move it to a common crate or can we just leave it there?
|
||||
fn get_ms_left_from_now(step: u128) -> u32 {
|
||||
#[allow(clippy::expect_used)]
|
||||
let dur = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.expect("invalid duration from epoch now");
|
||||
let ms: u128 = dur.as_millis();
|
||||
(step * 1000 - ms % (step * 1000)) as u32
|
||||
}
|
||||
}
|
||||
|
|
|
@ -602,6 +602,9 @@ pub enum RawOpt {
|
|||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum SelfOpt {
|
||||
/// Use the identify user feature
|
||||
#[clap(name = "identify-user")]
|
||||
IdentifyUser(CommonOpt),
|
||||
/// Show the current authenticated user's identity
|
||||
Whoami(CommonOpt),
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue