This commit is contained in:
Sebastiano Tocci 2023-08-23 12:51:24 +02:00 committed by GitHub
parent 753ef82a4b
commit 70b19f0630
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 1392 additions and 9 deletions

180
Cargo.lock generated
View file

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

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

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

View 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).
![idv api diagram](diagrams/idv_api_diagram.drawio.svg)
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.
![generic api responses](diagrams/idv_generic_responses.drawio.svg)
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 doesnt 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. Youll also notice some empty rectangles with a red border: they represent the scenario in which the other user tells us that the code provided doesnt match. This makes the idv fail, and its the only case in which the failure is entirely driven by the ui.
![idv state machine](diagrams/idv_state_machine.drawio.svg)

View file

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

View file

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

View file

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

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

View file

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

View file

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